diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 91a949d8b..0bf1beed0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -61,3 +61,5 @@ jobs: - name: Run integration tests run: uv run make test + env: + STORAGE_TESTS_ENABLED: "true" diff --git a/.gitignore b/.gitignore index cbb34bfb2..56cddbdd8 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/collections/ansible_collections/osac/config_as_code/roles/aap/templates/compute-instance-operations-ig.j2 b/collections/ansible_collections/osac/config_as_code/roles/aap/templates/compute-instance-operations-ig.j2 index a6420ac51..240b729a0 100644 --- a/collections/ansible_collections/osac/config_as_code/roles/aap/templates/compute-instance-operations-ig.j2 +++ b/collections/ansible_collections/osac/config_as_code/roles/aap/templates/compute-instance-operations-ig.j2 @@ -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: diff --git a/collections/ansible_collections/osac/config_as_code/roles/aap/templates/storage-operations-ig.j2 b/collections/ansible_collections/osac/config_as_code/roles/aap/templates/storage-operations-ig.j2 new file mode 100644 index 000000000..0e3724f89 --- /dev/null +++ b/collections/ansible_collections/osac/config_as_code/roles/aap/templates/storage-operations-ig.j2 @@ -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: + - 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 diff --git a/collections/ansible_collections/osac/config_as_code/roles/aap/vars/controller.yml b/collections/ansible_collections/osac/config_as_code/roles/aap/vars/controller.yml index 7dab44c0c..debeba4cc 100644 --- a/collections/ansible_collections/osac/config_as_code/roles/aap/vars/controller.yml +++ b/collections/ansible_collections/osac/config_as_code/roles/aap/vars/controller.yml @@ -327,6 +327,34 @@ controller_templates: # noqa: var-naming[no-role-prefix] 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" @@ -431,6 +459,9 @@ controller_inventories: # noqa: var-naming[no-role-prefix] - 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" @@ -487,6 +518,15 @@ controller_inventory_sources: # noqa: var-naming[no-role-prefix] 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 @@ -826,3 +866,7 @@ controller_instance_groups: # noqa: var-naming[no-role-prefix] - 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') }}" diff --git a/collections/ansible_collections/osac/service/plugins/filter/find_template_roles.py b/collections/ansible_collections/osac/service/plugins/filter/find_template_roles.py index 9027b1c8b..87b5ae1be 100644 --- a/collections/ansible_collections/osac/service/plugins/filter/find_template_roles.py +++ b/collections/ansible_collections/osac/service/plugins/filter/find_template_roles.py @@ -264,6 +264,7 @@ class TemplateTypeEnum(StrEnum): cluster = "cluster" compute_instance = "compute_instance" network = "network" + storage_provider = "storage_provider" class NetworkClassCapabilities(Base): @@ -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: diff --git a/collections/ansible_collections/osac/service/roles/storage_provider/meta/argument_specs.yaml b/collections/ansible_collections/osac/service/roles/storage_provider/meta/argument_specs.yaml new file mode 100644 index 000000000..cf7775c0d --- /dev/null +++ b/collections/ansible_collections/osac/service/roles/storage_provider/meta/argument_specs.yaml @@ -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. diff --git a/collections/ansible_collections/osac/service/roles/storage_provider/tasks/_dispatch_provider.yaml b/collections/ansible_collections/osac/service/roles/storage_provider/tasks/_dispatch_provider.yaml new file mode 100644 index 000000000..7367db546 --- /dev/null +++ b/collections/ansible_collections/osac/service/roles/storage_provider/tasks/_dispatch_provider.yaml @@ -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 }}" + 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' diff --git a/collections/ansible_collections/osac/service/roles/storage_provider/tasks/ensure_storage_class.yaml b/collections/ansible_collections/osac/service/roles/storage_provider/tasks/ensure_storage_class.yaml new file mode 100644 index 000000000..48dc7a804 --- /dev/null +++ b/collections/ansible_collections/osac/service/roles/storage_provider/tasks/ensure_storage_class.yaml @@ -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 }}" diff --git a/collections/ansible_collections/osac/service/roles/storage_provider/tasks/main.yaml b/collections/ansible_collections/osac/service/roles/storage_provider/tasks/main.yaml new file mode 100644 index 000000000..cc81ba738 --- /dev/null +++ b/collections/ansible_collections/osac/service/roles/storage_provider/tasks/main.yaml @@ -0,0 +1,111 @@ +--- +# Input validation fires here, before any include_tasks dispatch. +# This is the single point of validation — provider roles must never be called +# directly via include_role from playbooks, bypassing this dispatcher. +# Provider and protocol validation is NOT hardcoded here — providers are discovered +# dynamically (role must exist as osac.templates.{provider}_storage), and protocols +# are validated by each provider role via its meta/osac.yaml capabilities. + +- name: Validate storage_provider_tiers is a non-empty list + ansible.builtin.fail: + msg: >- + storage_provider_tiers must be a non-empty list of dicts with name, protocol, and provider. + Got: {{ storage_provider_tiers | default('undefined') | to_json }} + when: >- + storage_provider_tiers is not defined + or storage_provider_tiers | length == 0 + +- name: Validate each tier has required fields (name, protocol, provider) + ansible.builtin.fail: + msg: >- + Each tier in storage_provider_tiers must have 'name', 'protocol', and 'provider' fields. + Invalid tier: {{ item | to_json }} + loop: "{{ storage_provider_tiers }}" + when: >- + item.name is not defined or item.name | length == 0 + or item.protocol is not defined or item.protocol | length == 0 + or item.provider is not defined or item.provider | length == 0 + +- name: Validate tier names are valid DNS labels (lowercase alphanumeric + hyphens, 1-63 chars) + ansible.builtin.fail: + msg: >- + Tier name '{{ item.name }}' is not a valid DNS label. + Must be 1-63 characters, lowercase alphanumeric and hyphens only, + no leading or trailing hyphens. + loop: "{{ storage_provider_tiers }}" + when: item.name is not defined or not (item.name | regex_search('^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$')) + +- name: Validate tier names are unique across the entire list + ansible.builtin.fail: + msg: >- + storage_provider_tiers contains duplicate tier names. + Names must be unique. Got: {{ storage_provider_tiers | map(attribute='name') | list | to_json }} + when: >- + (storage_provider_tiers | map(attribute='name') | list | unique | length) + != (storage_provider_tiers | length) + +- name: Validate each tier's provider name is a valid DNS label + ansible.builtin.fail: + msg: >- + Invalid provider '{{ item.provider }}' in tier '{{ item.name }}'. + Must be a valid DNS label (1-63 chars, lowercase alphanumeric and hyphens). + loop: "{{ storage_provider_tiers }}" + when: item.provider is not regex('^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$') + +- name: Validate each tier's protocol is a non-empty string + ansible.builtin.fail: + msg: >- + Invalid protocol '{{ item.protocol }}' in tier '{{ item.name }}'. + Must be a non-empty string. Valid protocols are provider-specific + (declared in each provider role's meta/osac.yaml capabilities). + loop: "{{ storage_provider_tiers }}" + when: item.protocol | length == 0 + +- name: Validate qos_policy is a valid DNS label when specified + ansible.builtin.fail: + msg: >- + Tier '{{ item.name }}' has an invalid qos_policy: '{{ item.qos_policy }}'. + Must be a non-empty string matching DNS label rules (1-63 chars, lowercase alphanumeric and hyphens). + loop: "{{ storage_provider_tiers }}" + loop_control: + label: "{{ item.name }}" + when: + - item.qos_policy is defined + - item.qos_policy is not regex('^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$') + +- name: Validate storage_provider_provisioning_target is in the allowed enum + ansible.builtin.fail: + msg: >- + Invalid storage_provider_provisioning_target '{{ storage_provider_provisioning_target }}'. + Must be one of: vmaas, hcp_control_plane, hcp_worker_root, hcp_data_plane. + when: >- + storage_provider_provisioning_target not in + ['vmaas', 'hcp_control_plane', 'hcp_worker_root', 'hcp_data_plane'] + +- name: Validate tenant_name is set + ansible.builtin.fail: + msg: "tenant_name must be a non-empty string (set at play level from the Tenant CR or ComputeInstance tenant reference)." + when: tenant_name is not defined or tenant_name | length == 0 + +- name: Validate tenant_namespace is set + ansible.builtin.fail: + msg: "tenant_namespace must be a non-empty string (set at play level from the Tenant CR or ComputeInstance tenant reference)." + when: tenant_namespace is not defined or tenant_namespace | length == 0 + +- name: Validate storage_provider_action is supported + ansible.builtin.fail: + msg: >- + Invalid storage_provider_action '{{ storage_provider_action }}'. Must be one of: + setup, ensure_storage_class, teardown. + when: storage_provider_action not in ['setup', 'ensure_storage_class', 'teardown'] + +- name: Extract unique providers from tier list + ansible.builtin.set_fact: + _unique_providers: "{{ storage_provider_tiers | map(attribute='provider') | unique | sort | list }}" + +- name: Set dispatch action + ansible.builtin.set_fact: + _dispatch_action: "{{ storage_provider_action }}" + +- name: Dispatch to storage action task file + ansible.builtin.include_tasks: "{{ storage_provider_action }}.yaml" diff --git a/collections/ansible_collections/osac/service/roles/storage_provider/tasks/setup.yaml b/collections/ansible_collections/osac/service/roles/storage_provider/tasks/setup.yaml new file mode 100644 index 000000000..f59ec0bb1 --- /dev/null +++ b/collections/ansible_collections/osac/service/roles/storage_provider/tasks/setup.yaml @@ -0,0 +1,30 @@ +--- +# Dispatches setup to each unique provider in the tier list. +# Accumulates per-provider tenant configs into a single dict keyed by provider name. +# Credential clearing happens in the always block. + +- name: Initialize tenant config accumulator + ansible.builtin.set_fact: + _all_tenant_configs: {} + +- name: Dispatch setup to each provider + block: + - name: Run setup per provider + ansible.builtin.include_tasks: _dispatch_provider.yaml + loop: "{{ _unique_providers }}" + loop_control: + loop_var: _current_provider + + - name: Set merged tenant configs output + ansible.builtin.set_fact: + storage_provider_tenant_configs: "{{ _all_tenant_configs }}" + + - name: Validate each provider produced a tenant config + ansible.builtin.fail: + msg: >- + Provider '{{ item }}' did not set the required output fact + 'storage_provider_tenant_config' after setup. + loop: "{{ _unique_providers }}" + loop_control: + loop_var: item + when: item not in _all_tenant_configs diff --git a/collections/ansible_collections/osac/service/roles/storage_provider/tasks/teardown.yaml b/collections/ansible_collections/osac/service/roles/storage_provider/tasks/teardown.yaml new file mode 100644 index 000000000..a1346334a --- /dev/null +++ b/collections/ansible_collections/osac/service/roles/storage_provider/tasks/teardown.yaml @@ -0,0 +1,13 @@ +--- +# Dispatches teardown to each unique provider in the tier list. +# Best-effort cleanup — ignore_errors on the outer loop so one provider's +# failure doesn't block cleanup of other providers. + +- name: Dispatch teardown to each provider + block: + - name: Run teardown per provider + ansible.builtin.include_tasks: _dispatch_provider.yaml + loop: "{{ _unique_providers }}" + loop_control: + loop_var: _current_provider + ignore_errors: true # noqa: ignore-errors diff --git a/collections/ansible_collections/osac/service/roles/storage_provider/tests/test.yml b/collections/ansible_collections/osac/service/roles/storage_provider/tests/test.yml new file mode 100644 index 000000000..f705ec8d0 --- /dev/null +++ b/collections/ansible_collections/osac/service/roles/storage_provider/tests/test.yml @@ -0,0 +1,465 @@ +--- +# Unit tests for osac.service.storage_provider dispatcher validation logic. +# All 12 scenarios run in a single invocation -- no flags needed. +# Tests verify validation ONLY; they do not dispatch to actual provider roles. + +# ────────────────────────────────────────────────────────────── +# Test 1: Valid single tier passes all validation +# Expected: role passes validation, then fails at dispatch +# (because the action task file won't exist in unit test context) +# ────────────────────────────────────────────────────────────── +- name: Test storage_provider -- valid single tier passes validation + hosts: localhost + gather_facts: false + + tasks: + - name: Run valid-single-tier test + block: + - name: Include storage_provider with valid inputs + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_tiers: + - name: default + protocol: nfs + provider: vast + storage_provider_action: setup + storage_provider_provisioning_target: vmaas + tenant_name: "tenant-acme" + tenant_namespace: "osac-tenants" + + # If we get here, the dispatch step ran (validation passed). + # The dispatch itself may fail because the action task file + # requires provider roles, but that is fine -- validation passed. + - name: Validation passed (dispatch outcome is not under test) + ansible.builtin.debug: + msg: "Validation passed -- dispatch was attempted" + + rescue: + - name: Verify failure is from dispatch, not validation + ansible.builtin.assert: + that: + # Validation errors all contain specific keywords. + # A dispatch failure means validation succeeded. + - >- + 'must be a non-empty list' not in (ansible_failed_result.msg | default('')) + - >- + 'Must be one of' not in (ansible_failed_result.msg | default('')) + - >- + 'must be a non-empty string' not in (ansible_failed_result.msg | default('')) + - >- + 'not a valid DNS label' not in (ansible_failed_result.msg | default('')) + - >- + 'duplicate tier names' not in (ansible_failed_result.msg | default('')) + - >- + 'exceeds the maximum' not in (ansible_failed_result.msg | default('')) + fail_msg: >- + Validation failed unexpectedly: {{ ansible_failed_result.msg | default('unknown error') }} + success_msg: "Validation passed -- dispatch failed as expected in unit test context" + +# ────────────────────────────────────────────────────────────── +# Test 2: Empty tier list -- error path +# Expected: role fails with non-empty list message +# ────────────────────────────────────────────────────────────── +- name: Test storage_provider -- empty tier list (should fail) + hosts: localhost + gather_facts: false + + tasks: + - name: Run empty-tiers failure test + block: + - name: Attempt with empty tiers list (expected to fail) + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_tiers: [] + storage_provider_action: setup + tenant_name: "tenant-acme" + tenant_namespace: "osac-tenants" + + - name: This task should not be reached -- role should have failed + ansible.builtin.fail: + msg: "Role did not fail as expected when tiers list is empty" + + rescue: + - name: Verify role failed with expected empty-list message + ansible.builtin.assert: + that: + - "'must be a non-empty list' in ansible_failed_result.msg" + fail_msg: "Role failed but with unexpected message: {{ ansible_failed_result.msg | default('unknown') }}" + success_msg: "Role correctly rejected invalid input" + +# ────────────────────────────────────────────────────────────── +# Test 3: Missing required tier fields -- error path +# Expected: role fails with required fields message +# ────────────────────────────────────────────────────────────── +- name: Test storage_provider -- missing required tier fields (should fail) + hosts: localhost + gather_facts: false + + tasks: + - name: Run missing-fields failure test + block: + - name: Attempt with tier missing protocol and provider (expected to fail) + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_tiers: + - name: default + storage_provider_action: setup + tenant_name: "tenant-acme" + tenant_namespace: "osac-tenants" + + - name: This task should not be reached -- role should have failed + ansible.builtin.fail: + msg: "Role did not fail as expected when tier is missing required fields" + + rescue: + - name: Verify role failed with expected required-fields message + ansible.builtin.assert: + that: + - >- + 'protocol' in (ansible_failed_result.msg | default('')) + or 'provider' in (ansible_failed_result.msg | default('')) + or 'One or more items failed' in (ansible_failed_result.msg | default('')) + or 'must have' in (ansible_failed_result.msg | default('')) + fail_msg: "Role failed but with unexpected message: {{ ansible_failed_result.msg | default('unknown') }}" + success_msg: "Role correctly rejected tier with missing fields" + +# ────────────────────────────────────────────────────────────── +# Test 4: Invalid protocol -- error path +# Expected: role fails with protocol not in allowed list message +# ────────────────────────────────────────────────────────────── +- name: Test storage_provider -- invalid protocol (should fail) + hosts: localhost + gather_facts: false + + tasks: + - name: Run invalid-protocol failure test + block: + - name: Attempt with invalid protocol 'ftp' (expected to fail) + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_tiers: + - name: default + protocol: ftp + provider: vast + storage_provider_action: setup + tenant_name: "tenant-acme" + tenant_namespace: "osac-tenants" + + - name: This task should not be reached -- role should have failed + ansible.builtin.fail: + msg: "Role did not fail as expected when protocol is invalid" + + rescue: + - name: Verify role failed for invalid protocol + ansible.builtin.assert: + that: + - >- + 'unsupported protocol' in (ansible_failed_result.results | default([]) | map(attribute='msg') | join(' ') | lower) + or 'Invalid protocol' in (ansible_failed_result.results | default([]) | map(attribute='msg') | join(' ')) + or 'One or more items failed' in (ansible_failed_result.msg | default('')) + fail_msg: "Role failed but with unexpected message: {{ ansible_failed_result.msg | default('unknown') }}" + success_msg: "Role correctly rejected invalid protocol" + +# ────────────────────────────────────────────────────────────── +# Test 5: Invalid provider -- error path +# Expected: role fails at dispatch (provider role does not exist) +# ────────────────────────────────────────────────────────────── +- name: Test storage_provider -- invalid provider (should fail) + hosts: localhost + gather_facts: false + + tasks: + - name: Run invalid-provider failure test + block: + - name: Attempt with invalid provider 'netapp' (expected to fail) + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_tiers: + - name: default + protocol: nfs + provider: netapp + storage_provider_action: setup + tenant_name: "tenant-acme" + tenant_namespace: "osac-tenants" + + - name: This task should not be reached -- role should have failed + ansible.builtin.fail: + msg: "Role did not fail as expected when provider is invalid" + + rescue: + - name: Verify role failed for invalid provider + ansible.builtin.assert: + that: + - >- + 'osac.templates.netapp_storage' in (ansible_failed_result.msg | default('')) + or 'was not found' in (ansible_failed_result.msg | default('')) + or 'One or more items failed' in (ansible_failed_result.msg | default('')) + fail_msg: "Role failed but with unexpected message: {{ ansible_failed_result.msg | default('unknown') }}" + success_msg: "Role correctly rejected invalid provider" + +# ────────────────────────────────────────────────────────────── +# Test 6: Invalid action -- error path +# Expected: role fails with action not in allowed list message +# ────────────────────────────────────────────────────────────── +- name: Test storage_provider -- invalid action (should fail) + hosts: localhost + gather_facts: false + + tasks: + - name: Run invalid-action failure test + block: + - name: Attempt with invalid action 'destroy' (expected to fail) + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_tiers: + - name: default + protocol: nfs + provider: vast + storage_provider_action: destroy + tenant_name: "tenant-acme" + tenant_namespace: "osac-tenants" + + - name: This task should not be reached -- role should have failed + ansible.builtin.fail: + msg: "Role did not fail as expected when action is invalid" + + rescue: + - name: Verify role failed with expected invalid-action message + ansible.builtin.assert: + that: + - "'destroy' in (ansible_failed_result.msg | default(''))" + - "'setup' in (ansible_failed_result.msg | default('')) or 'must be one of' in (ansible_failed_result.msg | default(''))" + fail_msg: "Role failed but with unexpected message: {{ ansible_failed_result.msg | default('unknown') }}" + success_msg: "Role correctly rejected invalid input" + +# ────────────────────────────────────────────────────────────── +# Test 7: Invalid provisioning target -- error path +# Expected: role fails with provisioning target not in allowed list +# ────────────────────────────────────────────────────────────── +- name: Test storage_provider -- invalid provisioning target (should fail) + hosts: localhost + gather_facts: false + + tasks: + - name: Run invalid-provisioning-target failure test + block: + - name: Attempt with invalid provisioning target 'baremetal' (expected to fail) + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_tiers: + - name: default + protocol: nfs + provider: vast + storage_provider_action: setup + storage_provider_provisioning_target: baremetal + tenant_name: "tenant-acme" + tenant_namespace: "osac-tenants" + + - name: This task should not be reached -- role should have failed + ansible.builtin.fail: + msg: "Role did not fail as expected when provisioning target is invalid" + + rescue: + - name: Verify role failed with expected invalid-target message + ansible.builtin.assert: + that: + - "'baremetal' in (ansible_failed_result.msg | default(''))" + - "'vmaas' in (ansible_failed_result.msg | default('')) or 'must be one of' in (ansible_failed_result.msg | default(''))" + fail_msg: "Role failed but with unexpected message: {{ ansible_failed_result.msg | default('unknown') }}" + success_msg: "Role correctly rejected invalid input" + +# ────────────────────────────────────────────────────────────── +# Test 8: Duplicate tier names -- error path +# Expected: role fails with duplicate names message +# ────────────────────────────────────────────────────────────── +- name: Test storage_provider -- duplicate tier names (should fail) + hosts: localhost + gather_facts: false + + tasks: + - name: Run duplicate-tier-names failure test + block: + - name: Attempt with two tiers both named 'default' (expected to fail) + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_tiers: + - name: default + protocol: nfs + provider: vast + - name: default + protocol: block + provider: vast + storage_provider_action: setup + tenant_name: "tenant-acme" + tenant_namespace: "osac-tenants" + + - name: This task should not be reached -- role should have failed + ansible.builtin.fail: + msg: "Role did not fail as expected when tier names are duplicated" + + rescue: + - name: Verify role failed with expected duplicate-names message + ansible.builtin.assert: + that: + - "'duplicate tier names' in ansible_failed_result.msg" + fail_msg: "Role failed but with unexpected message: {{ ansible_failed_result.msg | default('unknown') }}" + success_msg: "Role correctly rejected invalid input" + +# ────────────────────────────────────────────────────────────── +# Test 9: Invalid DNS label tier name -- error path +# Expected: role fails with DNS label validation message +# ────────────────────────────────────────────────────────────── +- name: Test storage_provider -- invalid DNS label tier name (should fail) + hosts: localhost + gather_facts: false + + tasks: + - name: Run invalid-dns-label failure test + block: + - name: Attempt with tier name containing spaces and uppercase (expected to fail) + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_tiers: + - name: "My Tier" + protocol: nfs + provider: vast + storage_provider_action: setup + tenant_name: "tenant-acme" + tenant_namespace: "osac-tenants" + + - name: This task should not be reached -- role should have failed + ansible.builtin.fail: + msg: "Role did not fail as expected when tier name is not a valid DNS label" + + rescue: + - name: Verify role failed with expected DNS label message + ansible.builtin.assert: + that: + - >- + 'not a valid DNS label' in (ansible_failed_result.results | default([]) | map(attribute='msg') | join(' ')) + or 'One or more items failed' in (ansible_failed_result.msg | default('')) + fail_msg: "Role failed but with unexpected message: {{ ansible_failed_result.msg | default('unknown') }}" + success_msg: "Role correctly rejected invalid input" + +# ────────────────────────────────────────────────────────────── +# Test 10: Tier count exceeds maximum -- error path +# Expected: role fails with exceeds-maximum message +# ────────────────────────────────────────────────────────────── +- name: Test storage_provider -- tier count exceeds max (should fail) + hosts: localhost + gather_facts: false + + tasks: + - name: Run tier-count-exceeds-max failure test + block: + - name: Attempt with 3 tiers but max_tiers set to 2 (expected to fail) + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_tiers: + - name: tier-a + protocol: nfs + provider: vast + - name: tier-b + protocol: nfs + provider: vast + - name: tier-c + protocol: block + provider: vast + storage_provider_max_tiers: 2 + storage_provider_action: setup + tenant_name: "tenant-acme" + tenant_namespace: "osac-tenants" + + - name: This task should not be reached -- role should have failed + ansible.builtin.fail: + msg: "Role did not fail as expected when tier count exceeds maximum" + + rescue: + - name: Verify role failed with expected exceeds-maximum message + ansible.builtin.assert: + that: + - "'exceeds the maximum' in ansible_failed_result.msg" + - "'3' in ansible_failed_result.msg" + - "'2' in ansible_failed_result.msg" + fail_msg: "Role failed but with unexpected message: {{ ansible_failed_result.msg | default('unknown') }}" + success_msg: "Role correctly rejected invalid input" + +# ────────────────────────────────────────────────────────────── +# Test 11: Missing tenant_name -- error path +# Expected: role fails with tenant_name required message +# ────────────────────────────────────────────────────────────── +- name: Test storage_provider -- missing tenant_name (should fail) + hosts: localhost + gather_facts: false + + tasks: + - name: Run missing-tenant-name failure test + block: + - name: Attempt without tenant_name (expected to fail) + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_tiers: + - name: default + protocol: nfs + provider: vast + storage_provider_action: setup + tenant_namespace: "osac-tenants" + + - name: This task should not be reached -- role should have failed + ansible.builtin.fail: + msg: "Role did not fail as expected when tenant_name is missing" + + rescue: + - name: Verify role failed with expected tenant_name message + ansible.builtin.assert: + that: + - "'tenant_name' in ansible_failed_result.msg" + - "'non-empty string' in ansible_failed_result.msg" + fail_msg: "Role failed but with unexpected message: {{ ansible_failed_result.msg | default('unknown') }}" + success_msg: "Role correctly rejected invalid input" + +# ────────────────────────────────────────────────────────────── +# Test 12: Missing tenant_namespace -- error path +# Expected: role fails with tenant_namespace required message +# ────────────────────────────────────────────────────────────── +- name: Test storage_provider -- missing tenant_namespace (should fail) + hosts: localhost + gather_facts: false + + tasks: + - name: Run missing-tenant-namespace failure test + block: + - name: Attempt without tenant_namespace (expected to fail) + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_tiers: + - name: default + protocol: nfs + provider: vast + storage_provider_action: setup + tenant_name: "tenant-acme" + + - name: This task should not be reached -- role should have failed + ansible.builtin.fail: + msg: "Role did not fail as expected when tenant_namespace is missing" + + rescue: + - name: Verify role failed with expected tenant_namespace message + ansible.builtin.assert: + that: + - "'tenant_namespace' in ansible_failed_result.msg" + - "'non-empty string' in ansible_failed_result.msg" + fail_msg: "Role failed but with unexpected message: {{ ansible_failed_result.msg | default('unknown') }}" + success_msg: "Role correctly rejected invalid input" diff --git a/collections/ansible_collections/osac/templates/README.md b/collections/ansible_collections/osac/templates/README.md index e80a56bcc..b65683cb7 100644 --- a/collections/ansible_collections/osac/templates/README.md +++ b/collections/ansible_collections/osac/templates/README.md @@ -17,6 +17,7 @@ OSAC [fulfillment service](https://github.com/osac-project/fulfillment-service). - **Cluster Templates**: Deploy complete OpenShift clusters with customizable configurations - **VM Templates**: Provision virtual machines on OpenShift Virtualization with cloud-init support +- **Storage Provider Roles**: Provision storage backends for OSAC tenants and manage K8s StorageClasses ## Installation @@ -171,6 +172,116 @@ single file: `meta/osac.yaml`. See roles/ocp_virt_vm for more examples +### Creating a New Storage Provider Role + +Storage provider roles use a tier-based dispatch model: deployments define storage tiers +via `STORAGE_TIERS` JSON, each tier declares its provider, and the service-layer dispatcher +(`osac.service.storage_provider`) groups tiers by provider and dispatches to each provider's +template role with the filtered tier subset. + +1. Create role structure. The role **MUST** be named `{provider}_storage` (e.g., + `vast_storage`, `netapp_storage`). This naming convention is enforced by the + dispatcher at `osac.service.storage_provider`, which constructs the role name + dynamically: `osac.templates.{{ provider }}_storage`. Using a different naming + pattern will cause a runtime role-not-found error. + ```bash + mkdir -p roles/my_provider_storage/{tasks,defaults,meta} + ``` + +2. Define storage provider metadata in `roles/my_provider_storage/meta/osac.yaml`: + ```yaml + title: My Storage Provider + template_type: storage_provider + implementation_strategy: my_provider + capabilities: + supports_nfs: true + provisioning_targets: + - vmaas + ``` + +3. Implement the three required action task files. Each receives `_provider_tiers` + (filtered tier subset for this provider) and `_provisioning_target` via `vars:` + from the dispatcher: + - `tasks/setup.yaml` — provision storage backend resources per tier. + Must set `storage_provider_tenant_config` output fact (dict). + - `tasks/ensure_storage_class.yaml` — JIT K8s StorageClass provisioning per tier. + Must set `storage_provider_storage_class_names` (list of SC names, one per tier). + Must apply EP #26 labels (`osac.openshift.io/tenant`, `osac.openshift.io/storage-tier`). + - `tasks/teardown.yaml` — cleanup in reverse dependency order. + +4. Add the provider name to the hardcoded allowlist in + `osac.service.storage_provider/tasks/main.yaml`. The allowlist is hardcoded + (not a variable) to prevent override via extra_vars. + +5. **Credential isolation:** Admin credentials (e.g., VAST VMS admin) must NEVER + appear in tenant-namespace K8s Secrets. Provider roles must create per-tenant + data-plane credentials and use only those in CSI Secrets. Admin credentials + remain in the AAP-namespace IG Secret, injected via env vars. + +6. **Provisioning targets:** Each provider handles both VMaaS and CaaS provisioning + targets via the `_provisioning_target` parameter. Currently supported: `vmaas`. + CaaS targets (`hcp_control_plane`, `hcp_worker_root`, `hcp_data_plane`) are + defined in the enum but not yet implemented. + +**CaaS provisioning targets:** `hcp_control_plane`, `hcp_worker_root`, and +`hcp_data_plane` are defined in the provisioning target enum but not yet implemented. +When implemented, `hcp_data_plane` will support provisioning multiple StorageClasses +into the guest HCP cluster (e.g., separate tiers for databases and general workloads). +Currently, all CaaS targets return an explicit "not yet implemented" error. + +**Configuration:** Storage tiers are configured via the `STORAGE_TIERS` env var in the +`storage-operations-ig` ConfigMap: +```json +[ + {"name": "default", "protocol": "nfs", "provider": "vast", "qos_policy": "default-qos", + "qos_limits": {"static_limits": {"max_reads_bw_mbps": 100, "max_writes_bw_mbps": 100}}}, + {"name": "high-performance", "protocol": "block", "provider": "vast", "qos_policy": "perf-qos", + "qos_limits": {"static_limits": {"max_reads_bw_mbps": 500, "max_writes_bw_mbps": 500}}} +] +``` + +**Tier fields:** + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | yes | DNS-label tier name (used in StorageClass naming) | +| `protocol` | yes | `nfs` or `block` | +| `provider` | yes | Provider name (e.g., `vast`) | +| `qos_policy` | no | QoS policy name (creates STATIC mode policy on VMS) | +| `qos_limits` | no | Dict merged into QoS POST body (e.g., `static_limits`, `static_total_limits`) | +| `quota` | no | Hard quota in bytes for the tier's view | + +**QoS limits:** When `qos_policy` is specified, the role creates a QoS policy via REST API +(`POST /api/qospolicies/`). The `qos_limits` dict is merged directly into the POST body, so +its keys must match VMS API fields. Real VMS rejects STATIC mode without at least one limit — +always include `qos_limits.static_limits` when specifying `qos_policy`. + +**Dispatcher pattern:** `osac.service.storage_provider` validates inputs (tier list, +provider allowlist, protocol allowlist, provisioning target enum, max tier count) then +dispatches to `osac.templates.{provider}_storage` via a `_dispatch_provider.yaml` +wrapper (Ansible does not support `loop:` on `include_role`). + +**VAST provider specifics:** The `vast_storage` role creates per-tenant VMS managers +(TENANT_ADMIN user type) with random passwords via REST API (`POST /api/managers/`). +The vendored `vastdata.vms` collection has no module for managers, roles, QoS policies, +or API tokens — these 4 resources use `ansible.builtin.uri` directly. Per-tenant +credentials are stored in a hub-cluster Secret and used in CSI Secrets. Password +rotation requires teardown + re-provisioning. + +**Block encryption:** When `spec.blockEncryptionPassphrase` is provided in the Tenant CR +event payload, the passphrase is persisted to the hub Secret during `setup` and used by +`ensure_storage_class` to populate the CSI Secret's `passphrase` field and set +`hostEncryption: "true"` on block-protocol StorageClasses. On CSI Secret recreation, +`ensure_storage_class` reads the passphrase from the hub Secret so it survives the +original event. NFS encryption is managed at the VAST cluster level and is not +controlled per-StorageClass. + +**Legacy StorageClass migration:** Tenants provisioned before the multi-tier refactor +have single-tier StorageClasses (e.g., `vast-nfs-{tenant}`). The refactored code creates +new multi-tier-named StorageClasses (e.g., `vast-nfs-{tenant}-default`) alongside legacy +ones. Existing PVCs continue to reference legacy names; new workloads use tier-specific +names. + ## Architecture Templates integrate with OSAC through a well-defined interface: @@ -189,11 +300,23 @@ Templates integrate with OSAC through a well-defined interface: 4. OSAC assigns floating IP and configures port forwarding 5. On deletion, template removes all resources in order +### Storage Provider Lifecycle +1. Operator creates Org CR, triggering `{{ aap_prefix }}-create-org` AAP job +2. `setup` provisions VAST resources per tier (tenant, views, quotas) +3. Per-tenant VAST user created with random password (admin creds never leave AAP) +4. Tenant config + per-tenant credentials persisted to hub-cluster K8s Secret +5. VIP pool is pre-configured globally by infra admins (not provisioned per-tenant) +6. At VM creation, JIT `ensure_storage_class` checks if all tiers' StorageClasses exist (short-circuit) +7. If absent, reads per-tenant creds from hub Secret, creates CSI Secret with tenant creds + per-tier StorageClasses +8. On tenant deletion (`{{ aap_prefix }}-delete-org`), `teardown` reads stored config, deletes per-tier resources + per-tenant user +9. Teardown validates provider type, handles legacy single-tier Secrets, gates hub Secret deletion on cleanup success + ## Dependencies ### Runtime Dependencies -- `osac.service` collection (for cluster templates) -- `kubernetes.core` collection (for VM templates) +- `osac.service` collection (for cluster templates and storage provider dispatcher) +- `kubernetes.core` collection (for VM templates and storage K8s resource management) +- `vastdata.vms` collection v1.2.0 (for VAST storage provider, vendored) - `osac.esi` collection (for floating IP management) ### Environment Requirements diff --git a/collections/ansible_collections/osac/templates/roles/ocp_virt_vm/tasks/create_resources.yaml b/collections/ansible_collections/osac/templates/roles/ocp_virt_vm/tasks/create_resources.yaml index fd5e4f1f7..2264a8590 100644 --- a/collections/ansible_collections/osac/templates/roles/ocp_virt_vm/tasks/create_resources.yaml +++ b/collections/ansible_collections/osac/templates/roles/ocp_virt_vm/tasks/create_resources.yaml @@ -3,7 +3,7 @@ ansible.builtin.include_role: name: osac.service.tenant_storage_class vars: - tenant_storage_class_storage_tier: "default" + tenant_storage_class_storage_tier: "{{ _requested_storage_tier | default('default') }}" - name: Set storage_class from tenant StorageClass ansible.builtin.set_fact: diff --git a/collections/ansible_collections/osac/templates/roles/vast_storage/defaults/main.yaml b/collections/ansible_collections/osac/templates/roles/vast_storage/defaults/main.yaml new file mode 100644 index 000000000..83b9cb968 --- /dev/null +++ b/collections/ansible_collections/osac/templates/roles/vast_storage/defaults/main.yaml @@ -0,0 +1,38 @@ +--- +# VAST-specific defaults — all use vast_storage_ prefix. + +# Protocol-to-CSI-driver map used by ensure_csi_operator and ensure_storage_class. +# These must match the CSI driver names registered by the VAST CSI Operator +# (same names as the Helm chart defaults: csi.vastdata.com / block.csi.vastdata.com). +vast_storage_csi_provisioner_map: + nfs: "csi.vastdata.com" + block: "block.csi.vastdata.com" + +# K8s Secret on hub cluster that stores per-tenant VAST config +vast_storage_tenant_config_secret_prefix: "vast-tenant-config-" + +# Namespace for hub-cluster config secrets — set via OSAC_STORAGE_CONFIG_NAMESPACE env var +# in the storage-operations-ig pod spec (downward API metadata.namespace). +vast_storage_config_namespace: "{{ lookup('env', 'OSAC_STORAGE_CONFIG_NAMESPACE') | default('osac-system', true) }}" + +# TLS certificate validation for VAST VMS API calls. +vast_storage_validate_certs: "{{ lookup('env', 'VAST_VALIDATE_CERTS') | default('true', true) | bool }}" + +# API timeout (seconds) passed to the vastdata.vms modules via the vms: connection dict. +vast_storage_api_timeout: 30 + +# Shared VIP pool configuration — one pool for all tenants. +# Tenant isolation relies on source-based access control, not per-pool separation. +# The pool is created idempotently during the first tenant setup. +vast_storage_vip_pool_name: "{{ lookup('env', 'VAST_VIP_POOL_NAME') | default('osac-shared', true) }}" +vast_storage_vip_pool_ip_ranges: "{{ lookup('env', 'VAST_VIP_POOL_IP_RANGES') | default('', true) }}" +vast_storage_vip_pool_subnet_cidr: "{{ lookup('env', 'VAST_VIP_POOL_SUBNET_CIDR') | default('', true) }}" +vast_storage_vip_pool_gw_ip: "{{ lookup('env', 'VAST_VIP_POOL_GW_IP') | default('', true) }}" +vast_storage_vip_pool_gw_ipv6: "{{ lookup('env', 'VAST_VIP_POOL_GW_IPV6') | default('', true) }}" + +# CSI Operator installation via OLM +vast_storage_csi_operator_namespace: "vast-csi-system" +vast_storage_csi_operator_channel: "stable" +vast_storage_csi_operator_approval: "Automatic" +vast_storage_csi_operator_catalog_source: "certified-operators" +vast_storage_csi_operator_catalog_namespace: "openshift-marketplace" diff --git a/collections/ansible_collections/osac/templates/roles/vast_storage/meta/osac.yaml b/collections/ansible_collections/osac/templates/roles/vast_storage/meta/osac.yaml new file mode 100644 index 000000000..e93ac2ed3 --- /dev/null +++ b/collections/ansible_collections/osac/templates/roles/vast_storage/meta/osac.yaml @@ -0,0 +1,20 @@ +--- +title: VAST Data Storage Provider +description: > + Provisions VAST Data storage resources for OSAC tenants. + Creates VAST tenants, per-tier views/quotas, and per-tenant users via + vastdata.vms. Uses a globally-shared VIP pool. Manages K8s Secrets and + StorageClasses with VAST CSI parameters and EP #26 labels. Admin + credentials never enter tenant-namespace Secrets. + +template_type: storage_provider +implementation_strategy: vast +capabilities: + supported_protocols: + - nfs + - block + provisioning_targets: + - vmaas + - hcp_control_plane + - hcp_worker_root + - hcp_data_plane diff --git a/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/create_qos_policy.yaml b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/create_qos_policy.yaml new file mode 100644 index 000000000..197fb5835 --- /dev/null +++ b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/create_qos_policy.yaml @@ -0,0 +1,96 @@ +--- +# Creates per-tier QoS policies on VAST via REST API for tiers that define qos_policy. +# Uses ansible.builtin.uri because the vendored collection has no QoS module. +# +# Requires in scope: +# _vast_vms_conn — VAST VMS connection dict (admin or tenant manager) +# _vast_tenant_id — VAST tenant ID +# _provider_tiers — tier list from dispatcher +# +# Sets output fact: +# _vast_qos_policy_names — list of created QoS policy names + +- name: Filter tiers with qos_policy defined + ansible.builtin.set_fact: + _vast_qos_tiers: "{{ _provider_tiers | selectattr('qos_policy', 'defined') | list }}" + +- name: Skip QoS policy creation when no tiers define qos_policy + when: _vast_qos_tiers | length == 0 + block: + - name: Set empty QoS policy names + ansible.builtin.set_fact: + _vast_qos_policy_names: [] + + - name: Log QoS policy skip + ansible.builtin.debug: + msg: "No tiers define qos_policy — skipping QoS policy creation." + +- name: Create QoS policies for tiers that define them + when: _vast_qos_tiers | length > 0 + block: + - name: Authenticate to VAST VMS for QoS management + ansible.builtin.uri: + url: "https://{{ _vast_vms_conn.host }}/api/token/" + method: POST + body_format: json + body: + username: "{{ _vast_vms_conn.username }}" + password: "{{ _vast_vms_conn.password }}" + validate_certs: "{{ _vast_vms_conn.validate_certs }}" + timeout: "{{ _vast_vms_conn.timeout | int }}" + status_code: [200, 201] + register: _vast_qos_auth + no_log: true + + - name: Check if QoS policy already exists per tier + ansible.builtin.uri: + url: "https://{{ _vast_vms_conn.host }}/api/qospolicies/?name={{ item.qos_policy | urlencode }}" + method: GET + headers: + Authorization: "Bearer {{ _vast_qos_auth.json.access }}" + validate_certs: "{{ _vast_vms_conn.validate_certs }}" + timeout: "{{ _vast_vms_conn.timeout | int }}" + status_code: [200] + loop: "{{ _vast_qos_tiers }}" + loop_control: + label: "{{ item.qos_policy }}" + register: _vast_qos_check_results + no_log: true + + - name: Create QoS policies that do not yet exist + ansible.builtin.uri: + url: "https://{{ _vast_vms_conn.host }}/api/qospolicies/" + method: POST + body_format: json + body: >- + {{ {'name': item.item.qos_policy, + 'tenant_id': _vast_tenant_id | int, + 'mode': 'STATIC'} + | combine(item.item.qos_limits | default({})) }} + headers: + Authorization: "Bearer {{ _vast_qos_auth.json.access }}" + validate_certs: "{{ _vast_vms_conn.validate_certs }}" + timeout: "{{ _vast_vms_conn.timeout | int }}" + status_code: [200, 201] + loop: "{{ _vast_qos_check_results.results }}" + loop_control: + label: "{{ item.item.qos_policy }}" + when: item.json | length == 0 + register: _vast_qos_create_results + no_log: true + + - name: Set QoS policy names output fact + ansible.builtin.set_fact: + _vast_qos_policy_names: "{{ _vast_qos_tiers | map(attribute='qos_policy') | list }}" + + - name: Log QoS policy creation results + ansible.builtin.debug: + msg: "QoS policies ensured: {{ _vast_qos_policy_names | to_json }}" + + always: + - name: Clear QoS auth facts + ansible.builtin.set_fact: + _vast_qos_auth: {} + _vast_qos_check_results: {} + _vast_qos_create_results: {} + no_log: true diff --git a/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/create_quotas.yaml b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/create_quotas.yaml new file mode 100644 index 000000000..5bba3d6c8 --- /dev/null +++ b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/create_quotas.yaml @@ -0,0 +1,24 @@ +--- +# Creates per-tier VAST quotas using the vendored vastdata.vms.quotas module. +# Only creates quotas for tiers that define a quota field. +# Idempotent: the module handles create-or-update via state: present. +# +# Requires in scope: +# _vast_vms_conn — VAST VMS connection dict +# _vast_tenant_id — VAST tenant ID +# _provider_tiers — tier list from dispatcher +# tenant_name — play-level var + +- name: Create VAST quota per tier (when quota specified) + when: _provider_tiers | selectattr('quota', 'defined') | list | length > 0 + vastdata.vms.quotas: + vms: "{{ _vast_vms_conn }}" + name: "quota-{{ tenant_name }}-{{ item.name }}" + path: "/osac/{{ tenant_name }}/{{ item.name }}" + tenant_id: "{{ _vast_tenant_id }}" + hard_limit: "{{ item.quota }}" + state: present + loop: "{{ _provider_tiers | selectattr('quota', 'defined') | list }}" + loop_control: + label: "quota-{{ tenant_name }}-{{ item.name }}" + no_log: true diff --git a/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/create_tenant_manager.yaml b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/create_tenant_manager.yaml new file mode 100644 index 000000000..60f72b12b --- /dev/null +++ b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/create_tenant_manager.yaml @@ -0,0 +1,223 @@ +--- +# Creates a per-tenant VMS Manager with a CSI role for storage operations. +# The manager is NOT created as TENANT_ADMIN — it defaults to SUPER_ADMIN. +# TENANT_ADMIN users are blocked from /api/v1/versions/ (hard VMS restriction, +# not a permission issue), which the CSI driver's @requisite(semver="5.3.0") +# decorator requires before any storage operation. The tenant scoping comes +# from the role's tenant_id and the tenant_name StorageClass parameter instead. +# The CSI Secret intentionally omits the 'tenant' field — including it causes +# the CSI driver to set X-Tenant-Name on the /api/v1/token/ login call, which +# tenant-scopes the JWT and blocks /api/v1/versions/ even for SUPER_ADMIN users. +# +# Flow: auth as admin → create role → create manager. +# Uses ansible.builtin.uri because vastdata.vms has no modules for managers/roles. +# +# Requires in scope (set by setup.yaml before calling this file): +# vast_endpoint — VMS API hostname +# vast_username / vast_password — admin credentials +# vast_storage_validate_certs — TLS validation flag +# vast_storage_api_timeout — API timeout (seconds) +# _vast_tenant_result.tenants.id — VAST tenant ID +# tenant_name — play-level var +# vast_storage_tenant_config_secret_prefix — from defaults +# vast_storage_config_namespace — from defaults +# +# Sets output facts (cleared by setup.yaml always block): +# _vast_tenant_manager_name +# _vast_tenant_manager_username +# _vast_tenant_manager_password +# _vast_tenant_manager_id +# _vast_tenant_role_id + +- name: Assert VAST tenant ID is available + ansible.builtin.assert: + that: + - _vast_tenant_result is defined + - _vast_tenant_result.tenants.id is defined + - _vast_tenant_result.tenants.id | string | length > 0 + fail_msg: >- + _vast_tenant_result.tenants.id must be set before calling create_tenant_manager.yaml. + The VAST tenant must be created first. + +# Idempotency: check if hub Secret already has valid credentials. If so, skip. +- name: Check if tenant manager credentials already exist in hub Secret + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: "{{ vast_storage_tenant_config_secret_prefix }}{{ tenant_name }}" + namespace: "{{ vast_storage_config_namespace }}" + register: _vast_tenant_secret_check + no_log: true + +- name: Extract existing credentials if hub Secret already has them + when: + - _vast_tenant_secret_check.resources | length > 0 + - _vast_tenant_secret_check.resources[0].data.tenant_manager_username is defined + - _vast_tenant_secret_check.resources[0].data.tenant_manager_password is defined + - (_vast_tenant_secret_check.resources[0].data.tenant_manager_username | b64decode | length) > 0 + - (_vast_tenant_secret_check.resources[0].data.tenant_manager_password | b64decode | length) > 0 + ansible.builtin.set_fact: + _vast_tenant_manager_name: "{{ _vast_tenant_secret_check.resources[0].data.tenant_manager_name | default('') | b64decode }}" + _vast_tenant_manager_username: "{{ _vast_tenant_secret_check.resources[0].data.tenant_manager_username | b64decode }}" + _vast_tenant_manager_password: "{{ _vast_tenant_secret_check.resources[0].data.tenant_manager_password | b64decode }}" + _vast_tenant_manager_id: "{{ (_vast_tenant_secret_check.resources[0].data.tenant_manager_id | default('') | b64decode) }}" + _vast_tenant_role_id: "{{ (_vast_tenant_secret_check.resources[0].data.tenant_role_id | default('') | b64decode) }}" + _vast_tenant_manager_already_exists: true + no_log: true + +- name: Log idempotency skip when tenant manager already exists + when: _vast_tenant_manager_already_exists | default(false) + ansible.builtin.debug: + msg: "Tenant manager for '{{ tenant_name }}' already exists in hub Secret — skipping creation." + +- name: Create per-tenant VMS Manager with CSI role + when: not (_vast_tenant_manager_already_exists | default(false)) + block: + - name: Set tenant manager name + ansible.builtin.set_fact: + _vast_tenant_manager_name: "osac-{{ tenant_name }}" + + - name: Generate random password for tenant manager + ansible.builtin.set_fact: + _vast_tenant_manager_password: "{{ lookup('ansible.builtin.password', '/dev/null length=32 chars=ascii_letters,digits,!@#$%^&*()_+-=.~|{}[]:;<>?/') }}" + no_log: true + + - name: Set tenant manager username + ansible.builtin.set_fact: + _vast_tenant_manager_username: "osac-{{ tenant_name }}" + + - name: Authenticate to VAST VMS as admin + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/token/" + method: POST + body_format: json + body: + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200, 201] + register: _vast_admin_auth + no_log: true + + # Idempotency for partial failures: check if role already exists in VMS + - name: Check if tenant CSI role already exists + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/roles/?name=osac-csi-{{ tenant_name }}" + method: GET + headers: + Authorization: "Bearer {{ _vast_admin_auth.json.access }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200] + register: _vast_role_check + no_log: true + + - name: Create CSI role for per-tenant manager + when: _vast_role_check.json | length == 0 + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/roles/" + method: POST + body_format: json + body: + name: "osac-csi-{{ tenant_name }}" + # tenant_id: "{{ _vast_tenant_result.tenants.id | int }}" + permissions_list: + - create_logical + - view_logical + - edit_logical + - delete_logical + headers: + Authorization: "Bearer {{ _vast_admin_auth.json.access }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200, 201] + register: _vast_role_create_result + no_log: true + + - name: Set role ID from creation or existing lookup + ansible.builtin.set_fact: + _vast_tenant_role_id: >- + {{ (_vast_role_create_result.json.id | default('')) + if (_vast_role_check.json | length == 0) + else (_vast_role_check.json[0].id | default('')) }} + + - name: Fail if role ID is not available + ansible.builtin.fail: + msg: "Failed to create or find CSI role 'osac-csi-{{ tenant_name }}'. Check VMS API permissions." + when: _vast_tenant_role_id | string | length == 0 + + - name: Log CSI role status + ansible.builtin.debug: + msg: "CSI role 'osac-csi-{{ tenant_name }}' {{ 'created' if (_vast_role_check.json | length == 0) else 'already exists' }} (id: {{ _vast_tenant_role_id }})" + + # Idempotency for partial failures: check if manager already exists in VMS + - name: Check if tenant manager already exists in VMS + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/managers/?username=osac-{{ tenant_name }}" + method: GET + headers: + Authorization: "Bearer {{ _vast_admin_auth.json.access }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200] + register: _vast_manager_check + no_log: true + + - name: Create per-tenant manager (SUPER_ADMIN default, NOT TENANT_ADMIN) + when: _vast_manager_check.json | length == 0 + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/managers/" + method: POST + body_format: json + body: + username: "osac-{{ tenant_name }}" + password: "{{ _vast_tenant_manager_password }}" + # user_type: TENANT_ADMIN + # tenant_id: "{{ _vast_tenant_result.tenants.id | int }}" + roles: + - "{{ _vast_tenant_role_id | int }}" + password_expiration_disabled: true + is_temporary_password: false + headers: + Authorization: "Bearer {{ _vast_admin_auth.json.access }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200, 201] + register: _vast_manager_create_result + no_log: true + + - name: Set manager ID from creation or existing lookup + ansible.builtin.set_fact: + _vast_tenant_manager_id: >- + {{ (_vast_manager_create_result.json.id | default('')) + if (_vast_manager_check.json | length == 0) + else (_vast_manager_check.json[0].id | default('')) }} + + - name: Fail if manager ID is not available + ansible.builtin.fail: + msg: "Failed to create or find manager 'osac-{{ tenant_name }}'. Check VMS API permissions." + when: _vast_tenant_manager_id | string | length == 0 + + - name: Fail if manager already exists but password is unknown + when: _vast_manager_check.json | length > 0 + ansible.builtin.fail: + msg: >- + Manager 'osac-{{ tenant_name }}' already exists in VMS but the hub Secret + does not contain its credentials. Delete the manager in VMS and re-run, + or manually set the credentials in the hub Secret. + + always: + - name: Clear intermediate auth facts + ansible.builtin.set_fact: + _vast_admin_auth: {} + _vast_role_check: {} + _vast_role_create_result: {} + _vast_manager_check: {} + _vast_manager_create_result: {} + no_log: true + +- name: Log tenant manager creation outcome + ansible.builtin.debug: + msg: "VMS manager 'osac-{{ tenant_name }}' created with role 'osac-csi-{{ tenant_name }}'" + when: not (_vast_tenant_manager_already_exists | default(false)) diff --git a/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/create_view_policy.yaml b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/create_view_policy.yaml new file mode 100644 index 000000000..c2e973e36 --- /dev/null +++ b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/create_view_policy.yaml @@ -0,0 +1,48 @@ +--- +# Creates a per-tenant, per-protocol VAST view policy using the vendored +# vastdata.vms.viewpolicies module. +# The policy is named osac-{tenant_name}-{protocol} and uses NFS flavor. +# NFS flavor is used for all protocols because VAST only supports nfs and +# smb flavors. The VAST API requires a policy_id for all view creation +# (including block views), but block access control uses host NQN mappings +# — the NFS export rules in this policy are inert for block views. +# Idempotent: the module handles create-or-update via state: present. +# +# Requires in scope: +# _vast_vms_conn — VAST VMS connection dict +# _vast_tenant_id — VAST tenant ID (from hub Secret or tenant creation result) +# _vast_vp_protocol — protocol for this view policy (nfs or block) +# tenant_name — play-level var +# +# Accumulates into: +# _vast_view_policy_ids — dict mapping protocol → policy ID +# _vast_view_policy_names — dict mapping protocol → policy name + +- name: Set view policy name for protocol + ansible.builtin.set_fact: + _vast_view_policy_name: "osac-{{ tenant_name }}-{{ _vast_vp_protocol }}" + +- name: Create per-tenant view policy for {{ _vast_vp_protocol }} + vastdata.vms.viewpolicies: + vms: "{{ _vast_vms_conn }}" + name: "{{ _vast_view_policy_name }}" + flavor: NFS + tenant_id: "{{ _vast_tenant_id }}" + state: present + register: _vast_view_policy_result + no_log: true + +- name: Set view policy ID from result + ansible.builtin.set_fact: + _vast_view_policy_id: "{{ _vast_view_policy_result.viewpolicies.id }}" + +- name: Accumulate view policy ID and name by protocol + ansible.builtin.set_fact: + _vast_view_policy_ids: >- + {{ (_vast_view_policy_ids | default({})) | combine({_vast_vp_protocol: _vast_view_policy_id | string}) }} + _vast_view_policy_names: >- + {{ (_vast_view_policy_names | default({})) | combine({_vast_vp_protocol: _vast_view_policy_name}) }} + +- name: Log view policy creation result + ansible.builtin.debug: + msg: "View policy '{{ _vast_view_policy_name }}' ensured for {{ _vast_vp_protocol }} (id: {{ _vast_view_policy_id }})" diff --git a/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/create_views.yaml b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/create_views.yaml new file mode 100644 index 000000000..367b393f9 --- /dev/null +++ b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/create_views.yaml @@ -0,0 +1,33 @@ +--- +# Creates per-tier VAST views using the vendored vastdata.vms.views module. +# Idempotent: the module handles create-or-update via state: present. +# +# Requires in scope: +# _vast_vms_conn — VAST VMS connection dict +# _vast_tenant_id — VAST tenant ID +# _vast_view_policy_ids — dict mapping protocol → policy ID +# _provider_tiers — tier list from dispatcher +# tenant_name — play-level var + +- name: Create VAST view per tier + vastdata.vms.views: + vms: "{{ _vast_vms_conn }}" + name: "view-{{ tenant_name }}-{{ item.name }}" + path: "/osac/{{ tenant_name }}/{{ item.name }}" + create_dir: true + policy_id: "{{ _vast_view_policy_ids[item.protocol] }}" + tenant_id: "{{ _vast_tenant_id }}" + protocols: + - "{{ item.protocol | upper }}" + state: present + loop: "{{ _provider_tiers }}" + loop_control: + label: "view-{{ tenant_name }}-{{ item.name }}" + no_log: true + +- name: Log VAST view creation results + ansible.builtin.debug: + msg: "VAST view 'view-{{ tenant_name }}-{{ item.name }}' ensured at path '/osac/{{ tenant_name }}/{{ item.name }}'" + loop: "{{ _provider_tiers }}" + loop_control: + label: "view-{{ tenant_name }}-{{ item.name }}" diff --git a/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/delete_tenant_manager.yaml b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/delete_tenant_manager.yaml new file mode 100644 index 000000000..7b2f38be1 --- /dev/null +++ b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/delete_tenant_manager.yaml @@ -0,0 +1,120 @@ +--- +# Deletes the per-tenant VMS Manager and its CSI role during teardown. +# Uses ansible.builtin.uri because vastdata.vms has no modules for managers/roles. +# ignore_errors throughout — best-effort cleanup, resources may already be deleted. +# +# Requires in scope (set by teardown.yaml before calling this file): +# vast_endpoint — VMS API hostname +# vast_username / vast_password — admin credentials +# vast_storage_validate_certs — TLS validation flag +# vast_storage_api_timeout — API timeout (seconds) +# tenant_name — play-level var + +- name: Authenticate to VAST VMS as admin for manager cleanup + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/token/" + method: POST + body_format: json + body: + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200, 201] + register: _vast_admin_auth + ignore_errors: true + no_log: true + +- name: Skip manager cleanup if admin auth failed + when: _vast_admin_auth is failed + block: + - name: Set manager deletion failed flag (auth failure) + ansible.builtin.set_fact: + _vast_manager_delete_failed: true + + - name: Log admin auth failure + ansible.builtin.debug: + msg: "Cannot clean up VMS manager 'osac-{{ tenant_name }}' — admin auth failed (ignored)" + +- name: Clean up VMS manager resources + when: _vast_admin_auth is not failed + block: + # Look up manager to get its ID + - name: Look up tenant manager in VMS + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/managers/?username=osac-{{ tenant_name }}" + method: GET + headers: + Authorization: "Bearer {{ _vast_admin_auth.json.access }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200] + register: _vast_manager_lookup + ignore_errors: true + no_log: true + + # Delete the manager + - name: Delete tenant manager + when: + - _vast_manager_lookup is not failed + - _vast_manager_lookup.json | length > 0 + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/managers/{{ _vast_manager_lookup.json[0].id }}/" + method: DELETE + headers: + Authorization: "Bearer {{ _vast_admin_auth.json.access }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200, 204, 404] + register: _vast_manager_delete_result + ignore_errors: true + no_log: true + + # Delete the CSI role + - name: Look up tenant CSI role + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/roles/?name=osac-csi-{{ tenant_name }}" + method: GET + headers: + Authorization: "Bearer {{ _vast_admin_auth.json.access }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200] + register: _vast_role_lookup + ignore_errors: true + no_log: true + + - name: Delete tenant CSI role + when: + - _vast_role_lookup is not failed + - _vast_role_lookup.json | length > 0 + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/roles/{{ _vast_role_lookup.json[0].id }}/" + method: DELETE + headers: + Authorization: "Bearer {{ _vast_admin_auth.json.access }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200, 204, 404] + register: _vast_role_delete_result + ignore_errors: true + no_log: true + + - name: Set manager deletion result fact for caller + ansible.builtin.set_fact: + _vast_manager_delete_failed: >- + {{ (_vast_manager_delete_result is defined and _vast_manager_delete_result is failed) + or (_vast_manager_lookup is failed) }} + + - name: Clear auth facts + ansible.builtin.set_fact: + _vast_admin_auth: {} + _vast_manager_lookup: {} + _vast_role_lookup: {} + no_log: true + +- name: Log tenant manager deletion outcome + ansible.builtin.debug: + msg: >- + VMS manager 'osac-{{ tenant_name }}' deletion result: + {{ 'succeeded' if not (_vast_manager_delete_failed | default(false) | bool) else 'failed (ignored)' }} diff --git a/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/delete_vms_resources_by_tenant.yaml b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/delete_vms_resources_by_tenant.yaml new file mode 100644 index 000000000..3ce95d7ca --- /dev/null +++ b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/delete_vms_resources_by_tenant.yaml @@ -0,0 +1,53 @@ +--- +# Queries a VAST VMS resource type by tenant_id and deletes all matches. +# Reusable task file — called once per resource type (views, quotas, viewpolicies, qospolicies). +# +# Requires in scope: +# vast_endpoint — VMS API hostname +# vast_storage_validate_certs — TLS validation flag +# vast_storage_api_timeout — API timeout (seconds) +# _vast_cleanup_auth_token — Bearer token from admin auth +# _vast_cleanup_tenant_id — VAST tenant ID to filter by +# _vast_cleanup_resource — API resource name (e.g., "views", "quotas") + +- name: "Query VAST {{ _vast_cleanup_resource }} by tenant_id" + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/{{ _vast_cleanup_resource }}/?tenant_id={{ _vast_cleanup_tenant_id }}" + method: GET + headers: + Authorization: "Bearer {{ _vast_cleanup_auth_token }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200] + register: _vast_cleanup_query + ignore_errors: true + no_log: true + +- name: "Delete VAST {{ _vast_cleanup_resource }} by ID" + when: + - _vast_cleanup_query is not failed + - _vast_cleanup_query.json | default([]) | length > 0 + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/{{ _vast_cleanup_resource }}/{{ item.id }}/" + method: DELETE + headers: + Authorization: "Bearer {{ _vast_cleanup_auth_token }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200, 204, 404] + loop: "{{ _vast_cleanup_query.json | default([]) }}" + loop_control: + label: "{{ item.name | default(_vast_cleanup_resource ~ '-' ~ item.id) }}" + register: _vast_cleanup_delete + ignore_errors: true # noqa: ignore-errors + no_log: true + +- name: "Set {{ _vast_cleanup_resource }} cleanup result" + ansible.builtin.set_fact: + _vast_cleanup_results: >- + {{ (_vast_cleanup_results | default({})) | combine({ + _vast_cleanup_resource: { + 'queried': _vast_cleanup_query.json | default([]) | length, + 'failed': (_vast_cleanup_delete.results | default([]) | selectattr('failed', 'defined') | selectattr('failed') | list | length) > 0 + } + }) }} diff --git a/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/ensure_csi_operator.yaml b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/ensure_csi_operator.yaml new file mode 100644 index 000000000..d49fda51b --- /dev/null +++ b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/ensure_csi_operator.yaml @@ -0,0 +1,140 @@ +--- +# Ensures the VAST CSI Operator is installed via OLM and all required CSI drivers are registered. +# Self-healing: checks CSIDriver existence per protocol first, installs only when any are absent. +# Idempotent: OLM Subscription and VastCSIDriver CR creation are state: present. +# +# Requires: +# _vast_required_protocols — list of unique protocols from caller (e.g. ['nfs', 'block']) +# vast_storage_csi_provisioner_map — from defaults ({nfs: csi.vastdata.com, block: block.csi.vastdata.com}) +# vast_storage_csi_operator_* — from defaults (OLM config) + +- name: Build list of expected CSI provisioner names from required protocols + ansible.builtin.set_fact: + _vast_required_provisioners: >- + {{ + _vast_required_protocols | map('extract', vast_storage_csi_provisioner_map) | list + }} + +- name: Check which required VAST CSI drivers are already installed + kubernetes.core.k8s_info: + api_version: storage.k8s.io/v1 + kind: CSIDriver + name: "{{ item | trim }}" + loop: "{{ _vast_required_provisioners }}" + register: _vast_csi_driver_checks + +- name: Determine if any required CSI drivers are missing + ansible.builtin.set_fact: + _vast_any_driver_missing: >- + {{ + _vast_csi_driver_checks.results + | selectattr('resources', 'equalto', []) + | list + | length > 0 + }} + +- name: Log CSI drivers already installed + when: not _vast_any_driver_missing + ansible.builtin.debug: + msg: "All required VAST CSI drivers are already installed: {{ _vast_required_provisioners | join(', ') }}" + +- name: Install VAST CSI Operator via OLM (covers all protocols with single subscription) + when: _vast_any_driver_missing + block: + - name: Check if OLM is available on cluster + kubernetes.core.k8s_info: + api_version: operators.coreos.com/v1alpha1 + kind: CatalogSource + namespace: "{{ vast_storage_csi_operator_catalog_namespace }}" + register: _vast_olm_check + ignore_errors: true + + - name: Fail if OLM is not available and CSI driver installation is needed + when: _vast_olm_check.failed | default(false) + ansible.builtin.fail: + msg: >- + Required VAST CSI drivers ({{ _vast_required_provisioners | join(', ') }}) are not installed + and OLM (Operator Lifecycle Manager) is not available on this cluster. + Install OLM or deploy the VAST CSI drivers manually. + + - name: Warn if OLM is available but no CatalogSources found + when: + - not (_vast_olm_check.failed | default(false)) + - _vast_olm_check.resources | length == 0 + ansible.builtin.debug: + msg: >- + OLM API is reachable but no CatalogSources found in {{ vast_storage_csi_operator_catalog_namespace }}. + The VAST CSI Operator Subscription may fail to install. Ensure the + {{ vast_storage_csi_operator_catalog_source }} CatalogSource is available. + + - name: Ensure VAST CSI Operator namespace exists + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ vast_storage_csi_operator_namespace }}" + + - name: Ensure OperatorGroup exists for OwnNamespace install mode + kubernetes.core.k8s: + state: present + definition: + apiVersion: operators.coreos.com/v1 + kind: OperatorGroup + metadata: + name: vast-csi-operator-group + namespace: "{{ vast_storage_csi_operator_namespace }}" + spec: + targetNamespaces: + - "{{ vast_storage_csi_operator_namespace }}" + + - name: Ensure VAST CSI Operator Subscription exists + kubernetes.core.k8s: + state: present + definition: + apiVersion: operators.coreos.com/v1alpha1 + kind: Subscription + metadata: + name: vast-csi-operator + namespace: "{{ vast_storage_csi_operator_namespace }}" + spec: + channel: "{{ vast_storage_csi_operator_channel }}" + name: vast-csi-operator + source: "{{ vast_storage_csi_operator_catalog_source }}" + sourceNamespace: "{{ vast_storage_csi_operator_catalog_namespace }}" + installPlanApproval: "{{ vast_storage_csi_operator_approval }}" + + - name: Wait for VastCSIDriver CRD to be available + kubernetes.core.k8s_info: + api_version: apiextensions.k8s.io/v1 + kind: CustomResourceDefinition + name: vastcsidrivers.storage.vastdata.com + register: _vast_crd_check + retries: 30 + delay: 10 + until: _vast_crd_check.resources | length > 0 + + - name: Ensure VastCSIDriver CR exists for each required protocol + kubernetes.core.k8s: + state: present + definition: + apiVersion: storage.vastdata.com/v1 + kind: VastCSIDriver + metadata: + name: "{{ vast_storage_csi_provisioner_map[item] }}" + namespace: "{{ vast_storage_csi_operator_namespace }}" + spec: + driverType: "{{ item }}" + loop: "{{ _vast_required_protocols }}" + + - name: Wait for all required VAST CSI drivers to be registered + kubernetes.core.k8s_info: + api_version: storage.k8s.io/v1 + kind: CSIDriver + name: "{{ item | trim }}" + loop: "{{ _vast_required_provisioners }}" + register: _vast_csi_driver_wait + retries: 30 + delay: 10 + until: _vast_csi_driver_wait.resources | length > 0 diff --git a/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/ensure_storage_class.yaml b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/ensure_storage_class.yaml new file mode 100644 index 000000000..fa4978db0 --- /dev/null +++ b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/ensure_storage_class.yaml @@ -0,0 +1,332 @@ +--- +# VAST ensure_storage_class: creates CSI Secret + per-tier StorageClasses. +# CRITICAL SECURITY: CSI Secret uses per-tenant user credentials ONLY. +# Admin credentials are loaded (via read_credentials.yaml) for VMS API calls +# but are NEVER written into the CSI Secret or any tenant-namespace resource. + +- name: Fail if provisioning target is not yet supported + when: _provisioning_target != 'vmaas' + ansible.builtin.fail: + msg: >- + VAST storage provisioning for target '{{ _provisioning_target }}' is not yet + implemented. Only 'vmaas' is currently supported. + +- 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: Compute unique protocols from tiers + ansible.builtin.set_fact: + _vast_required_protocols: "{{ _provider_tiers | map(attribute='protocol') | unique | sort | list }}" + +- name: Build expected StorageClass entries per tier + ansible.builtin.set_fact: + _vast_expected_sc_entries: >- + {% set entries = [] -%} + {% for tier in _provider_tiers -%} + {% set _ = entries.append({'name': 'vast-' ~ tier.protocol ~ '-' ~ tenant_name ~ '-' ~ tier.name, 'tier': tier.name}) -%} + {% endfor -%} + {{ entries }} + _vast_expected_sc_names: >- + {% set names = [] -%} + {% for tier in _provider_tiers -%} + {% set _ = names.append('vast-' ~ tier.protocol ~ '-' ~ tenant_name ~ '-' ~ tier.name) -%} + {% endfor -%} + {{ names }} + +# Short-circuit -- check if ALL tiers' StorageClasses already exist (single API call) +- 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: _vast_sc_checks + +- name: Determine if all StorageClasses already exist + ansible.builtin.set_fact: + _vast_all_sc_exist: >- + {{ (_vast_expected_sc_names | difference( + _vast_sc_checks.resources | map(attribute='metadata.name') | list + ) | length) == 0 }} + +- name: Set output facts and skip provisioning when all SCs exist + when: _vast_all_sc_exist | bool + block: + - name: Set StorageClass names output (short-circuit) + ansible.builtin.set_fact: + storage_provider_storage_class_names: "{{ _vast_expected_sc_entries }}" + + - name: Log short-circuit + ansible.builtin.debug: + msg: "All {{ _vast_expected_sc_names | length }} StorageClasses already exist — skipping provisioning." + +# --- Full provisioning path --- +- name: Provision StorageClasses and CSI resources + when: not (_vast_all_sc_exist | bool) + block: + - name: Read per-tenant credentials and config from hub Secret + ansible.builtin.include_tasks: read_tenant_credentials.yaml + + - name: Ensure VAST CSI Operator is installed + ansible.builtin.include_tasks: ensure_csi_operator.yaml + + # ── Ensure per-tier VMS resources exist (views, quotas, QoS, view policies) ── + # These are created here (not just in setup) so JIT tiers get their backing + # VMS resources. Uses admin creds for VMS API access. + + - name: Resolve VAST admin credentials from env vars + ansible.builtin.include_tasks: read_credentials.yaml + + - name: Build VMS connection from admin credentials + ansible.builtin.set_fact: + _vast_vms_conn: + host: "{{ vast_endpoint }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout }}" + no_log: true + + - name: Initialize view policy accumulators + ansible.builtin.set_fact: + _vast_view_policy_ids: {} + _vast_view_policy_names: {} + + - name: Ensure per-protocol view policies exist + ansible.builtin.include_tasks: create_view_policy.yaml + loop: "{{ _vast_required_protocols }}" + loop_control: + loop_var: _vast_vp_protocol + + - name: Ensure VAST views exist per tier + ansible.builtin.include_tasks: create_views.yaml + + - name: Ensure per-tier QoS policies exist + ansible.builtin.include_tasks: create_qos_policy.yaml + + - name: Ensure VAST quotas exist per tier + ansible.builtin.include_tasks: create_quotas.yaml + + # ── K8s resources: CSI Secret, StorageClasses, VolumeSnapshotClasses ── + + - name: Set CSI Secret resource name + ansible.builtin.set_fact: + _vast_csi_secret_name: "vast-csi-{{ tenant_name }}" + + - name: Set block encryption passphrase from caller or persisted hub Secret + ansible.builtin.set_fact: + _vast_block_encryption_passphrase: "{{ storage_provider_block_encryption_passphrase | default(_vast_tenant_block_encryption_passphrase | default('')) }}" + no_log: true + + # CSI Secret uses per-tenant Manager username/password — NEVER admin creds. + # The 'tenant' field is intentionally OMITTED. Including it causes the CSI + # driver to set X-Tenant-Name on the /api/v1/token/ login call, which + # tenant-scopes the JWT and blocks GET /api/v1/versions/ — a prerequisite + # check the driver makes before every storage operation. Tenant scoping is + # handled by the tenant_name StorageClass parameter and the role permissions. + - name: Build CSI Secret data + ansible.builtin.set_fact: + _vast_csi_secret_data: + username: "{{ _vast_tenant_csi_username }}" + password: "{{ _vast_tenant_csi_password }}" + endpoint: "{{ _vast_tenant_endpoint }}" + # tenant: "{{ tenant_name }}" + no_log: true + + - name: Add passphrase to CSI Secret data for block encryption + ansible.builtin.set_fact: + _vast_csi_secret_data: "{{ _vast_csi_secret_data | combine({'passphrase': _vast_block_encryption_passphrase}) }}" + no_log: true + when: _vast_block_encryption_passphrase | default('') | length > 0 + + - name: Create CSI Secret with per-tenant manager credentials in tenant namespace + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Secret + metadata: + name: "{{ _vast_csi_secret_name }}" + namespace: "{{ tenant_namespace }}" + labels: + app.kubernetes.io/managed-by: osac-aap + osac.openshift.io/tenant: "{{ tenant_name }}" + type: Opaque + stringData: "{{ _vast_csi_secret_data }}" + no_log: true + + - name: Build CSI secret references (single source for all SC and VSC parameters) + ansible.builtin.set_fact: + _vast_csi_secret_ref: + name: "{{ _vast_csi_secret_name }}" + namespace: "{{ tenant_namespace }}" + + - name: Build base StorageClass parameters (shared by NFS and block) + ansible.builtin.set_fact: + _vast_sc_base_params: + vip_pool_name: "{{ _vast_tenant_vip_pool }}" + csi.storage.k8s.io/provisioner-secret-name: "{{ _vast_csi_secret_ref.name }}" + csi.storage.k8s.io/provisioner-secret-namespace: "{{ _vast_csi_secret_ref.namespace }}" + csi.storage.k8s.io/controller-publish-secret-name: "{{ _vast_csi_secret_ref.name }}" + csi.storage.k8s.io/controller-publish-secret-namespace: "{{ _vast_csi_secret_ref.namespace }}" + csi.storage.k8s.io/node-publish-secret-name: "{{ _vast_csi_secret_ref.name }}" + csi.storage.k8s.io/node-publish-secret-namespace: "{{ _vast_csi_secret_ref.namespace }}" + csi.storage.k8s.io/controller-expand-secret-name: "{{ _vast_csi_secret_ref.name }}" + csi.storage.k8s.io/controller-expand-secret-namespace: "{{ _vast_csi_secret_ref.namespace }}" + csi.storage.k8s.io/node-expand-secret-name: "{{ _vast_csi_secret_ref.name }}" + csi.storage.k8s.io/node-expand-secret-namespace: "{{ _vast_csi_secret_ref.namespace }}" + + - name: Build block host encryption JSON when passphrase is available + ansible.builtin.set_fact: + _vast_sc_block_encryption_params: + host_encryption: >- + {{ {'cipher': 'aes-xts-plain64', 'key_size': '512', + 'hash': 'sha256', 'pbkdf_memory': '65536'} | to_json }} + when: + - _vast_block_encryption_passphrase | default('') | length > 0 + - _vast_required_protocols | intersect(['block']) | length > 0 + + - name: Filter NFS tiers + ansible.builtin.set_fact: + _vast_nfs_tiers: "{{ _provider_tiers | selectattr('protocol', 'equalto', 'nfs') | list }}" + + - name: Filter block tiers + ansible.builtin.set_fact: + _vast_block_tiers: "{{ _provider_tiers | selectattr('protocol', 'equalto', 'block') | list }}" + + - name: Resolve per-protocol view policy names + ansible.builtin.set_fact: + _vast_nfs_view_policy: >- + {{ (_vast_view_policy_names['nfs'] | default(_vast_view_policy_name)) + if _vast_view_policy_names is defined + else _vast_view_policy_name }} + _vast_block_view_policy: >- + {{ (_vast_view_policy_names['block'] | default(_vast_view_policy_name)) + if _vast_view_policy_names is defined + else _vast_view_policy_name }} + + - name: Create NFS StorageClass per tier + when: _vast_nfs_tiers | length > 0 + kubernetes.core.k8s: + state: present + definition: + apiVersion: storage.k8s.io/v1 + kind: StorageClass + metadata: + name: "vast-{{ item.protocol }}-{{ tenant_name }}-{{ item.name }}" + labels: + app.kubernetes.io/managed-by: osac-aap + osac.openshift.io/tenant: "{{ tenant_name }}" + osac.openshift.io/storage-tier: "{{ item.name }}" + osac.openshift.io/storage-protocol: "{{ item.protocol }}" + provisioner: "{{ vast_storage_csi_provisioner_map[item.protocol] }}" + parameters: >- + {{ _vast_sc_base_params + | combine({'root_export': '/osac/' ~ tenant_name ~ '/' ~ item.name}) + | combine({'view_policy': _vast_nfs_view_policy}) + | combine(({'qos_policy': item.qos_policy} if item.qos_policy is defined else {})) }} + reclaimPolicy: Delete + volumeBindingMode: Immediate + loop: "{{ _vast_nfs_tiers }}" + loop_control: + label: "vast-{{ item.protocol }}-{{ tenant_name }}-{{ item.name }}" + + - name: Create block StorageClass per tier + when: _vast_block_tiers | length > 0 + kubernetes.core.k8s: + state: present + definition: + apiVersion: storage.k8s.io/v1 + kind: StorageClass + metadata: + name: "vast-{{ item.protocol }}-{{ tenant_name }}-{{ item.name }}" + labels: + app.kubernetes.io/managed-by: osac-aap + osac.openshift.io/tenant: "{{ tenant_name }}" + osac.openshift.io/storage-tier: "{{ item.name }}" + osac.openshift.io/storage-protocol: "{{ item.protocol }}" + provisioner: "{{ vast_storage_csi_provisioner_map[item.protocol] }}" + parameters: >- + {{ _vast_sc_base_params + | combine({'subsystem': 'view-' ~ tenant_name ~ '-' ~ item.name, + 'tenant_name': tenant_name}) + | combine({'csi.storage.k8s.io/node-stage-secret-name': _vast_csi_secret_ref.name}) + | combine({'csi.storage.k8s.io/node-stage-secret-namespace': _vast_csi_secret_ref.namespace}) + | combine(_vast_sc_block_encryption_params | default({})) + | combine(({'qos_policy': item.qos_policy} if item.qos_policy is defined else {})) }} + reclaimPolicy: Delete + volumeBindingMode: WaitForFirstConsumer + loop: "{{ _vast_block_tiers }}" + loop_control: + label: "vast-{{ item.protocol }}-{{ tenant_name }}-{{ item.name }}" + + - name: Check if VolumeSnapshot CRD is installed + when: storage_provider_snapshots_enabled | default(true) + kubernetes.core.k8s_info: + api_version: apiextensions.k8s.io/v1 + kind: CustomResourceDefinition + name: volumesnapshotclasses.snapshot.storage.k8s.io + register: _vast_vsc_crd_check + + - name: Create VolumeSnapshotClass per tier + when: >- + (storage_provider_snapshots_enabled | default(true)) and + (_vast_vsc_crd_check.resources | default([]) | length > 0) + kubernetes.core.k8s: + state: present + definition: + apiVersion: snapshot.storage.k8s.io/v1 + kind: VolumeSnapshotClass + metadata: + name: "vast-snapshot-{{ tenant_name }}-{{ item.name }}" + labels: + app.kubernetes.io/managed-by: osac-aap + osac.openshift.io/tenant: "{{ tenant_name }}" + osac.openshift.io/storage-tier: "{{ item.name }}" + driver: "{{ vast_storage_csi_provisioner_map[item.protocol] }}" + deletionPolicy: Delete + parameters: + csi.storage.k8s.io/snapshotter-secret-name: "{{ _vast_csi_secret_ref.name }}" + csi.storage.k8s.io/snapshotter-secret-namespace: "{{ _vast_csi_secret_ref.namespace }}" + loop: "{{ _provider_tiers }}" + loop_control: + label: "vast-snapshot-{{ tenant_name }}-{{ item.name }}" + + - name: Set StorageClass names output (list) + ansible.builtin.set_fact: + storage_provider_storage_class_names: "{{ _vast_expected_sc_entries }}" + + always: + # Clear per-tenant credential facts (admin creds were never loaded) + - name: Clear per-tenant credential facts from play scope + ansible.builtin.set_fact: + vast_endpoint: "" + vast_username: "" + vast_password: "" + _vast_tenant_id: "" + _vast_tenant_csi_username: "" + _vast_tenant_csi_password: "" + _vast_tenant_endpoint: "" + _vast_tenant_vip_pool: "" + _vast_tenant_config_secret: {} + _vast_vms_conn: {} + _vast_block_encryption_passphrase: "" + _vast_csi_secret_data: {} + _vast_csi_secret_ref: {} + _vast_view_policy_name: "" + _vast_view_policy_names: {} + _vast_view_policy_names_raw: "" + _vast_view_policy_ids: {} + _vast_tenant_block_encryption_passphrase: "" + _vast_sc_block_encryption_params: {} + _vast_nfs_tiers: [] + _vast_block_tiers: [] + _vast_nfs_view_policy: "" + _vast_block_view_policy: "" + no_log: true diff --git a/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/read_credentials.yaml b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/read_credentials.yaml new file mode 100644 index 000000000..e55ef473e --- /dev/null +++ b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/read_credentials.yaml @@ -0,0 +1,57 @@ +--- +# Reads VAST admin credentials from environment variables ONLY. +# Admin credentials are injected via AAP vault -> IG Secret -> pod env vars. +# There is NO K8s Secret fallback — admin credentials must never be stored +# in K8s Secrets accessible via the K8s API. + +- name: Read VAST credentials from environment variables + ansible.builtin.set_fact: + _vast_endpoint_env: "{{ lookup('env', 'VAST_ENDPOINT') }}" + _vast_username_env: "{{ lookup('env', 'VAST_USERNAME') }}" + _vast_password_env: "{{ lookup('env', 'VAST_PASSWORD') }}" + no_log: true + +- name: Check if all required env var credentials are available + ansible.builtin.set_fact: + _vast_env_creds_available: >- + {{ _vast_endpoint_env | length > 0 and + _vast_username_env | length > 0 and + _vast_password_env | length > 0 }} + _vast_env_creds_partial: >- + {{ (_vast_endpoint_env | length > 0 or + _vast_username_env | length > 0 or + _vast_password_env | length > 0) and + not (_vast_endpoint_env | length > 0 and + _vast_username_env | length > 0 and + _vast_password_env | length > 0) }} + no_log: true + +- name: Warn about partial env var credentials + when: _vast_env_creds_partial | bool + ansible.builtin.debug: + msg: >- + Partial VAST credential env vars detected — not all of VAST_ENDPOINT, + VAST_USERNAME, VAST_PASSWORD are set. All three must be provided together. + +- name: Fail if VAST admin credentials are not available + when: not (_vast_env_creds_available | bool) + ansible.builtin.fail: + msg: >- + VAST admin credentials not found. VAST_ENDPOINT, VAST_USERNAME, and + VAST_PASSWORD environment variables must all be set in the IG pod spec. + These are injected via AAP vault into the storage-operations-ig Secret + referenced in the IG pod spec envFrom block. + +- name: Set VAST credentials from environment variables + ansible.builtin.set_fact: + vast_endpoint: "{{ _vast_endpoint_env }}" + vast_username: "{{ _vast_username_env }}" + vast_password: "{{ _vast_password_env }}" + no_log: true + +- name: Clear intermediate env credential facts + ansible.builtin.set_fact: + _vast_endpoint_env: "" + _vast_username_env: "" + _vast_password_env: "" + no_log: true diff --git a/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/read_tenant_credentials.yaml b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/read_tenant_credentials.yaml new file mode 100644 index 000000000..47a7eae70 --- /dev/null +++ b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/read_tenant_credentials.yaml @@ -0,0 +1,84 @@ +--- +# Reads per-tenant VMS Manager credentials from the hub-cluster tenant config Secret. +# This is NOT the admin credentials reader (read_credentials.yaml reads admin creds from env vars). +# This file reads the per-tenant Manager username/password generated during setup. +# The CSI driver authenticates via username/password, not API tokens. +# +# CALLER CONTRACT: After using this file, callers MUST clear these facts in their always block: +# _vast_tenant_csi_username, _vast_tenant_csi_password, _vast_tenant_vip_pool, +# _vast_tenant_endpoint, _vast_view_policy_name, _vast_view_policy_names, +# _vast_view_policy_names_raw, _vast_tenant_block_encryption_passphrase +# Failure to clear leaves credentials in play-scoped facts for the remainder of the play. +# +# Requires: +# tenant_name — play-level var +# vast_storage_tenant_config_secret_prefix — from defaults +# vast_storage_config_namespace — from defaults +# +# Sets output facts (caller clears in always block): +# _vast_tenant_id +# _vast_tenant_csi_username +# _vast_tenant_csi_password +# _vast_tenant_vip_pool +# _vast_tenant_endpoint +# _vast_view_policy_name +# _vast_tenant_block_encryption_passphrase + +- name: Read tenant config Secret from hub cluster + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: "{{ vast_storage_tenant_config_secret_prefix }}{{ tenant_name }}" + namespace: "{{ vast_storage_config_namespace }}" + register: _vast_tenant_config_secret + no_log: true + +- name: Fail if tenant config Secret does not exist + ansible.builtin.fail: + msg: >- + Tenant config Secret '{{ vast_storage_tenant_config_secret_prefix }}{{ tenant_name }}' + not found in namespace '{{ vast_storage_config_namespace }}'. + Run setup for this tenant before calling ensure_storage_class. + when: _vast_tenant_config_secret.resources | length == 0 + +- name: Fail if tenant config Secret is missing manager credentials + ansible.builtin.fail: + msg: >- + Hub Secret for tenant '{{ tenant_name }}' does not contain per-tenant manager + credentials (tenant_manager_username or tenant_manager_password is missing or empty). + Re-run setup to create a per-tenant VMS Manager. + Do NOT fall back to admin credentials — that would reintroduce the security + vulnerability this architecture eliminates. + when: >- + _vast_tenant_config_secret.resources[0].data.tenant_manager_username is not defined + or (_vast_tenant_config_secret.resources[0].data.tenant_manager_username | b64decode | length) == 0 + or _vast_tenant_config_secret.resources[0].data.tenant_manager_password is not defined + or (_vast_tenant_config_secret.resources[0].data.tenant_manager_password | b64decode | length) == 0 + +- name: Extract per-tenant credentials and config from hub Secret + ansible.builtin.set_fact: + _vast_tenant_id: "{{ _vast_tenant_config_secret.resources[0].data.vast_tenant_id | b64decode }}" + _vast_tenant_csi_username: "{{ _vast_tenant_config_secret.resources[0].data.tenant_manager_username | b64decode }}" + _vast_tenant_csi_password: "{{ _vast_tenant_config_secret.resources[0].data.tenant_manager_password | b64decode }}" + _vast_tenant_vip_pool: "{{ _vast_tenant_config_secret.resources[0].data.vip_pool_name | b64decode }}" + _vast_tenant_endpoint: "{{ _vast_tenant_config_secret.resources[0].data.vast_endpoint | b64decode }}" + _vast_view_policy_name: "{{ _vast_tenant_config_secret.resources[0].data.view_policy_name | default('') | b64decode }}" + _vast_tenant_block_encryption_passphrase: "{{ _vast_tenant_config_secret.resources[0].data.block_encryption_passphrase | default('') | b64decode | default('', true) }}" + _vast_view_policy_names_raw: "{{ _vast_tenant_config_secret.resources[0].data.view_policy_names | default('') | b64decode | default('', true) }}" + no_log: true + +- name: Parse per-protocol view policy names when available + ansible.builtin.set_fact: + _vast_view_policy_names: >- + {{ _vast_view_policy_names_raw if (_vast_view_policy_names_raw is mapping) + else (_vast_view_policy_names_raw | from_json) }} + when: _vast_view_policy_names_raw | length > 0 + no_log: true + +- name: Log view policy name status + ansible.builtin.debug: + msg: "View policy from hub Secret: {{ _vast_view_policy_name | default('not set — will be created by ensure_storage_class') }}" + +- name: Clear raw tenant config Secret object from facts + ansible.builtin.set_fact: + _vast_tenant_config_secret: {} diff --git a/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/setup.yaml b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/setup.yaml new file mode 100644 index 000000000..23ef8062a --- /dev/null +++ b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/setup.yaml @@ -0,0 +1,290 @@ +--- +# VAST setup: multi-tier resource provisioning with per-tenant user credentials. +# Creates: VAST tenant, per-tier views, per-tier quotas, per-tenant user. +# Persists tenant config + credentials to hub Secret. +# Hub Secret write is INSIDE the block so rescue can clean up the user on failure. +# +# VIP pool: Uses a globally-shared pool (vast_storage_vip_pool_name) instead of +# per-tenant pools. The pool is created idempotently on the first tenant setup +# using VAST_VIP_POOL_IP_RANGES and VAST_VIP_POOL_SUBNET_CIDR from the IG ConfigMap. +# Tenant isolation relies on source-based access control. +# Limitation: All tenants share the same pool IP addresses. For dedicated +# per-tenant IPs, future work could add IP range allocation from an IPAM source. + +- 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: Fail if provisioning target is not yet supported + when: _provisioning_target != 'vmaas' + ansible.builtin.fail: + msg: >- + VAST storage provisioning for target '{{ _provisioning_target }}' is not yet + implemented. Only 'vmaas' is currently supported. + +- name: Validate tenant_name is a valid DNS label + ansible.builtin.fail: + msg: >- + tenant_name '{{ tenant_name }}' is not a valid DNS label. + Must be 1-63 lowercase alphanumeric characters or hyphens, + cannot start or end with a hyphen. + when: tenant_name is not regex('^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$') + +- name: Validate tier names are unique + ansible.builtin.assert: + that: + - (_provider_tiers | map(attribute='name') | list | unique | length) == (_provider_tiers | length) + fail_msg: "Tier names must be unique." + quiet: true + +- name: Validate tier protocols are supported by VAST provider + ansible.builtin.fail: + msg: >- + Tier '{{ item.name }}' has unsupported protocol '{{ item.protocol }}'. + VAST provider supports: nfs, block. + loop: "{{ _provider_tiers }}" + loop_control: + label: "{{ item.name }}" + when: item.protocol not in ['nfs', 'block'] + +- name: Validate tier quotas (when specified) + ansible.builtin.fail: + msg: >- + Tier '{{ item.name }}' has an invalid quota: '{{ item.quota }}'. + Quota must be a positive number (bytes). + loop: "{{ _provider_tiers }}" + loop_control: + label: "{{ item.name }}" + when: + - item.quota is defined + - (item.quota | int) <= 0 + +- name: Compute unique protocols from tiers + ansible.builtin.set_fact: + _vast_required_protocols: "{{ _provider_tiers | map(attribute='protocol') | unique | sort | list }}" + +- name: Provision VAST resources for tenant + block: + - name: Resolve VAST admin credentials from env vars + ansible.builtin.include_tasks: read_credentials.yaml + + - name: Build VAST VMS connection dict + ansible.builtin.set_fact: + _vast_vms_conn: + host: "{{ vast_endpoint }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout }}" + no_log: true + + - name: Validate VIP pool IP ranges are configured + ansible.builtin.fail: + msg: >- + VAST_VIP_POOL_IP_RANGES must be set in the storage-operations-ig ConfigMap. + Example: '[["10.0.100.10","10.0.100.50"]]' (JSON array of [start, end] pairs). + when: vast_storage_vip_pool_ip_ranges | length == 0 + + - name: Validate VIP pool subnet CIDR is configured + ansible.builtin.fail: + msg: >- + VAST_VIP_POOL_SUBNET_CIDR must be set in the storage-operations-ig ConfigMap. + This is the subnet prefix length (e.g., 24 for a /24 network). + when: vast_storage_vip_pool_subnet_cidr | string | length == 0 + + - name: Parse VIP pool IP ranges from JSON + ansible.builtin.set_fact: + _vast_vip_pool_ip_ranges: >- + {{ vast_storage_vip_pool_ip_ranges if (vast_storage_vip_pool_ip_ranges is not string) + else (vast_storage_vip_pool_ip_ranges | from_json) }} + + - name: Ensure shared VIP pool exists + vastdata.vms.vippools: + vms: + host: "{{ vast_endpoint }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout }}" + name: "{{ vast_storage_vip_pool_name }}" + ip_ranges: "{{ _vast_vip_pool_ip_ranges }}" + subnet_cidr: "{{ vast_storage_vip_pool_subnet_cidr | int }}" + gw_ip: "{{ vast_storage_vip_pool_gw_ip if (vast_storage_vip_pool_gw_ip | default('') | length > 0) else omit }}" + gw_ipv6: "{{ vast_storage_vip_pool_gw_ipv6 if (vast_storage_vip_pool_gw_ipv6 | default('') | length > 0) else omit }}" + role: PROTOCOLS + state: present + register: _vast_vip_pool_result + no_log: true + + - name: Log shared VIP pool status + ansible.builtin.debug: + msg: "Shared VIP pool '{{ vast_storage_vip_pool_name }}' ensured (id: {{ _vast_vip_pool_result.vippools.id | default('existing') }})" + + - name: Ensure VAST CSI Operator is installed + ansible.builtin.include_tasks: ensure_csi_operator.yaml + + - name: Provision VAST VMS resources + block: + - name: Create VAST tenant + vastdata.vms.tenants: + vms: "{{ _vast_vms_conn }}" + name: "{{ tenant_name }}" + state: present + register: _vast_tenant_result + no_log: true + + - name: Fail if VAST tenant creation did not return an ID + ansible.builtin.fail: + msg: >- + VAST tenant creation for '{{ tenant_name }}' did not return an ID. + Check VAST VMS API connectivity and permissions. + when: >- + _vast_tenant_result.tenants.id is not defined + or _vast_tenant_result.tenants.id | string | length == 0 + + - name: Log VAST tenant creation result + ansible.builtin.debug: + msg: "VAST tenant '{{ tenant_name }}' provisioned with ID: {{ _vast_tenant_result.tenants.id }}" + + - name: Set tenant ID for downstream tasks + ansible.builtin.set_fact: + _vast_tenant_id: "{{ _vast_tenant_result.tenants.id | string }}" + + - name: Resolve local_provider_id from VAST tenant result + ansible.builtin.set_fact: + _vast_local_provider_id: "{{ _vast_tenant_result.tenants.local_provider_id }}" + + - name: Create per-tenant VMS manager with CSI role + ansible.builtin.include_tasks: create_tenant_manager.yaml + + - name: Persist tenant config to K8s Secret on hub cluster + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Secret + metadata: + name: "{{ vast_storage_tenant_config_secret_prefix }}{{ tenant_name }}" + namespace: "{{ vast_storage_config_namespace }}" + labels: + app.kubernetes.io/managed-by: osac-aap + osac.openshift.io/tenant: "{{ tenant_name }}" + type: Opaque + stringData: + vast_tenant_id: "{{ _vast_tenant_result.tenants.id | string }}" + vast_local_provider_id: "{{ _vast_local_provider_id | string }}" + vip_pool_name: "{{ vast_storage_vip_pool_name }}" + vast_endpoint: "{{ vast_endpoint }}" + storage_provider_type: "vast" + tenant_manager_name: "{{ _vast_tenant_manager_name }}" + tenant_manager_username: "{{ _vast_tenant_manager_username }}" + tenant_manager_password: "{{ _vast_tenant_manager_password }}" + tenant_manager_id: "{{ _vast_tenant_manager_id | default('') | string }}" + tenant_role_id: "{{ _vast_tenant_role_id | default('') | string }}" + block_encryption_passphrase: "{{ storage_provider_block_encryption_passphrase | default('') }}" + no_log: true + + rescue: + # Rollback uses live VMS queries to find and delete all tenant resources. + - name: Rollback - delete per-tenant VMS manager + ansible.builtin.include_tasks: delete_tenant_manager.yaml + when: _vast_tenant_manager_name is defined and _vast_tenant_manager_name | length > 0 + ignore_errors: true # noqa: ignore-errors + + - name: Rollback - delete hub Secret + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "{{ vast_storage_tenant_config_secret_prefix }}{{ tenant_name }}" + namespace: "{{ vast_storage_config_namespace }}" + ignore_errors: true # noqa: ignore-errors + + - name: Rollback - authenticate for VMS cleanup + when: _vast_vms_conn is defined + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/token/" + method: POST + body_format: json + body: + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200, 201] + register: _vast_rollback_auth + ignore_errors: true + no_log: true + + - name: Rollback - clean up VMS resources via live tenant_id queries + when: + - _vast_vms_conn is defined + - _vast_rollback_auth is not failed + - _vast_tenant_result is defined + - _vast_tenant_result.tenants.id is defined + ansible.builtin.include_tasks: delete_vms_resources_by_tenant.yaml + loop: + - views + - quotas + - viewpolicies + - qospolicies + loop_control: + loop_var: _vast_cleanup_resource + vars: + _vast_cleanup_auth_token: "{{ _vast_rollback_auth.json.access }}" + _vast_cleanup_tenant_id: "{{ _vast_tenant_result.tenants.id }}" + ignore_errors: true # noqa: ignore-errors + + - name: Rollback - delete VAST tenant (retry for async cleanup) + vastdata.vms.tenants: + vms: "{{ _vast_vms_conn }}" + name: "{{ tenant_name }}" + state: absent + force_remove: true + retries: 3 + delay: 10 + until: _vast_rollback_tenant_result is not failed + register: _vast_rollback_tenant_result + ignore_errors: true # noqa: ignore-errors + no_log: true + when: _vast_vms_conn is defined + + - name: Report VAST API failure with tenant context + ansible.builtin.fail: + msg: >- + VAST API operation failed for tenant '{{ tenant_name }}'. + Failed task: {{ ansible_failed_task.name | default('unknown') }}. + Error: {{ ansible_failed_result.msg | default('see logs for details') }}. + Rollback attempted for partially-created resources. + + always: + # Clear all credential facts from play scope. + # NOTE: _vast_tenant_result is intentionally NOT cleared here because the + # storage_provider_tenant_config output fact (set after this always block) + # references _vast_tenant_result.tenants.id. Clearing it would produce empty output. + - name: Clear all credential facts from play scope + ansible.builtin.set_fact: + vast_endpoint: "" + vast_username: "" + vast_password: "" + _vast_vms_conn: {} + + _vast_tenant_id: "" + _vast_tenant_manager_name: "" + _vast_tenant_manager_username: "" + _vast_tenant_manager_password: "" + _vast_tenant_manager_id: "" + _vast_tenant_role_id: "" + _vast_local_provider_id: "" + no_log: true + +- name: Set storage_provider_tenant_config output fact + ansible.builtin.set_fact: + storage_provider_tenant_config: + vast_tenant_id: "{{ _vast_tenant_result.tenants.id | string }}" + vip_pool_name: "{{ vast_storage_vip_pool_name }}" diff --git a/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/teardown.yaml b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/teardown.yaml new file mode 100644 index 000000000..09e147563 --- /dev/null +++ b/collections/ansible_collections/osac/templates/roles/vast_storage/tasks/teardown.yaml @@ -0,0 +1,272 @@ +--- +# VAST teardown: live-query cleanup of all tenant resources. +# K8s resources are deleted by label selector (catches JIT-created SCs). +# VMS resources are queried live by tenant_id (no reliance on static tier list). + +- name: Fail if provisioning target is not yet supported + when: _provisioning_target != 'vmaas' + ansible.builtin.fail: + msg: >- + VAST storage provisioning for target '{{ _provisioning_target }}' is not yet + implemented. Only 'vmaas' is currently supported. + +- 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: Read tenant config Secret from hub cluster + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: "{{ vast_storage_tenant_config_secret_prefix }}{{ tenant_name }}" + namespace: "{{ vast_storage_config_namespace }}" + register: _vast_teardown_config + no_log: true + failed_when: false + +- name: Extract tenant ID from hub Secret (if available) + when: + - _vast_teardown_config.resources | length > 0 + - _vast_teardown_config.resources[0].data.vast_tenant_id is defined + ansible.builtin.set_fact: + _vast_teardown_tenant_id: "{{ _vast_teardown_config.resources[0].data.vast_tenant_id | b64decode }}" + no_log: true + +- name: Cleanup VAST resources + block: + - name: Resolve VAST admin credentials for VMS API cleanup + ansible.builtin.include_tasks: read_credentials.yaml + + - name: Build VAST VMS connection dict (cluster-scoped) + ansible.builtin.set_fact: + _vast_vms_conn: + host: "{{ vast_endpoint }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout }}" + no_log: true + + - name: Look up tenant ID live from VMS when hub Secret is missing + when: _vast_teardown_tenant_id is not defined + block: + - name: Look up VAST tenant by name + vastdata.vms.tenants: + vms: "{{ _vast_vms_conn }}" + name: "{{ tenant_name }}" + state: present + register: _vast_teardown_tenant_lookup + ignore_errors: true + no_log: true + + - name: Set tenant ID from live lookup + when: + - _vast_teardown_tenant_lookup is not failed + - _vast_teardown_tenant_lookup.tenants.id is defined + ansible.builtin.set_fact: + _vast_teardown_tenant_id: "{{ _vast_teardown_tenant_lookup.tenants.id | string }}" + + # ── K8s resources: delete by label selector ── + + - name: Find all 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: _vast_teardown_sc_list + + - name: Delete tenant StorageClasses + kubernetes.core.k8s: + state: absent + api_version: storage.k8s.io/v1 + kind: StorageClass + name: "{{ item.metadata.name }}" + loop: "{{ _vast_teardown_sc_list.resources | default([]) }}" + loop_control: + label: "{{ item.metadata.name }}" + ignore_errors: true # noqa: ignore-errors + + - name: Find all tenant VolumeSnapshotClasses by label + kubernetes.core.k8s_info: + api_version: snapshot.storage.k8s.io/v1 + kind: VolumeSnapshotClass + label_selectors: + - "osac.openshift.io/tenant={{ tenant_name }}" + - "app.kubernetes.io/managed-by=osac-aap" + register: _vast_teardown_vsc_list + ignore_errors: true # noqa: ignore-errors + + - name: Delete tenant VolumeSnapshotClasses + when: _vast_teardown_vsc_list.resources is defined + kubernetes.core.k8s: + state: absent + api_version: snapshot.storage.k8s.io/v1 + kind: VolumeSnapshotClass + name: "{{ item.metadata.name }}" + loop: "{{ _vast_teardown_vsc_list.resources | default([]) }}" + loop_control: + label: "{{ item.metadata.name }}" + ignore_errors: true # noqa: ignore-errors + + - name: Delete CSI Secret in tenant namespace + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "vast-csi-{{ tenant_name }}" + namespace: "{{ tenant_namespace }}" + ignore_errors: true # noqa: ignore-errors + no_log: true + + # ── VMS resources: query live by tenant_id ── + + - name: Authenticate to VAST VMS as admin + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/token/" + method: POST + body_format: json + body: + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200, 201] + register: _vast_teardown_auth + ignore_errors: true + no_log: true + + - name: Skip VMS resource cleanup if admin auth failed + when: _vast_teardown_auth is failed + ansible.builtin.debug: + msg: "Cannot clean up VMS resources — admin auth failed (ignored)" + + - name: Initialize VMS cleanup results + when: + - _vast_teardown_auth is not failed + - _vast_teardown_tenant_id is defined + ansible.builtin.set_fact: + _vast_cleanup_results: {} + + - name: Clean up VMS resources via live tenant_id queries + when: + - _vast_teardown_auth is not failed + - _vast_teardown_tenant_id is defined + ansible.builtin.include_tasks: delete_vms_resources_by_tenant.yaml + loop: + - views + - quotas + - viewpolicies + - qospolicies + loop_control: + loop_var: _vast_cleanup_resource + vars: + _vast_cleanup_auth_token: "{{ _vast_teardown_auth.json.access }}" + _vast_cleanup_tenant_id: "{{ _vast_teardown_tenant_id }}" + + - name: Delete per-tenant VMS manager + ansible.builtin.include_tasks: delete_tenant_manager.yaml + + - name: Delete VAST tenant (retry for async view cleanup) + vastdata.vms.tenants: + vms: "{{ _vast_vms_conn }}" + name: "{{ tenant_name }}" + state: absent + force_remove: true + register: _vast_teardown_tenant_result + retries: 3 + delay: 10 + until: _vast_teardown_tenant_result is not failed + ignore_errors: true # noqa: ignore-errors + no_log: true + + - name: Clean up orphaned local provider after tenant deletion + when: + - _vast_teardown_auth is defined + - _vast_teardown_auth is not failed + block: + - name: Query local provider by tenant name + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/localproviders/?name=provider-{{ tenant_name }}" + method: GET + headers: + Authorization: "Bearer {{ _vast_teardown_auth.json.access }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200] + register: _vast_teardown_lp + ignore_errors: true + no_log: true + + - name: Delete orphaned local provider + when: + - _vast_teardown_lp is not failed + - _vast_teardown_lp.json | default([]) | length > 0 + ansible.builtin.uri: + url: "https://{{ vast_endpoint }}/api/localproviders/{{ _vast_teardown_lp.json[0].id }}/" + method: DELETE + headers: + Authorization: "Bearer {{ _vast_teardown_auth.json.access }}" + validate_certs: "{{ vast_storage_validate_certs }}" + timeout: "{{ vast_storage_api_timeout | int }}" + status_code: [200, 204, 404] + ignore_errors: true # noqa: ignore-errors + no_log: true + + - name: Set VAST cleanup success flag + ansible.builtin.set_fact: + _vast_teardown_clean: >- + {{ not (_vast_cleanup_results | default({}) | dict2items | selectattr('value.failed', 'defined') | selectattr('value.failed') | list | length > 0) + and not (_vast_teardown_tenant_result.failed | default(false)) + and not (_vast_manager_delete_failed | default(false)) }} + + - name: Delete tenant config Secret from hub cluster (only when cleanup succeeded) + when: >- + _vast_teardown_config.resources | length > 0 and + (_vast_teardown_clean | bool) + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "{{ vast_storage_tenant_config_secret_prefix }}{{ tenant_name }}" + namespace: "{{ vast_storage_config_namespace }}" + no_log: true + + - name: Warn if tenant config Secret preserved due to cleanup failures + when: >- + _vast_teardown_config.resources | length > 0 and + not (_vast_teardown_clean | bool) + ansible.builtin.debug: + msg: >- + Tenant config Secret preserved because one or more resources failed to delete. + Re-run teardown to retry (idempotent). + + always: + - name: Clear all credential facts from play scope + ansible.builtin.set_fact: + vast_endpoint: "" + vast_username: "" + vast_password: "" + _vast_vms_conn: {} + _vast_tenant_manager_name: "" + _vast_tenant_manager_username: "" + _vast_tenant_manager_password: "" + _vast_tenant_manager_id: "" + _vast_tenant_role_id: "" + _vast_local_provider_id: "" + _vast_teardown_config: {} + _vast_teardown_auth: {} + _vast_teardown_tenant_id: "" + no_log: true + +- name: Report teardown summary + ansible.builtin.debug: + msg: >- + Teardown for tenant '{{ tenant_name }}' complete. + K8s: {{ (_vast_teardown_sc_list.resources | default([]) | length) }} SCs, + {{ (_vast_teardown_vsc_list.resources | default([]) | length) }} VSCs deleted. + VMS: {% for r, v in (_vast_cleanup_results | default({})).items() %}{{ v.queried }} {{ r }}{{ ', ' if not loop.last else '' }}{% endfor %} deleted. + Tenant: {{ 'ok' if not (_vast_teardown_tenant_result.failed | default(false)) else 'failed' }}, + Manager: {{ 'ok' if not (_vast_manager_delete_failed | default(false)) else 'failed' }}. diff --git a/collections/ansible_collections/osac/workflows/playbooks/compute_instance/create.yml b/collections/ansible_collections/osac/workflows/playbooks/compute_instance/create.yml index a42374fb7..ae41ba6f0 100644 --- a/collections/ansible_collections/osac/workflows/playbooks/compute_instance/create.yml +++ b/collections/ansible_collections/osac/workflows/playbooks/compute_instance/create.yml @@ -68,6 +68,19 @@ name: osac.service.tenant_target_namespace tasks: + # Guard: surfaces the ocp_virt_vm assertion (create_resources.yaml) earlier with a clearer message. + # This workflow is an internal implementation detail invoked via import_playbook — JIT storage + # provisioning only runs in the top-level playbook (playbook_osac_create_compute_instance.yml). + # Direct invocation of this workflow bypasses JIT, so this guard catches that case explicitly. + - name: Fail if tenant_storage_class_name is not set before template call + ansible.builtin.fail: + msg: >- + tenant_storage_class_name is not defined or empty. Direct invocation of this workflow + bypasses JIT storage provisioning (osac.service.storage_provider ensure_storage_class). + Either invoke via playbook_osac_create_compute_instance.yml (recommended) or set + tenant_storage_class_name explicitly before calling this workflow. + when: tenant_storage_class_name is not defined or tenant_storage_class_name | length == 0 + - name: Display compute instance information ansible.builtin.debug: msg: diff --git a/collections/requirements.yml b/collections/requirements.yml index 00a0c810e..9a7a1b842 100644 --- a/collections/requirements.yml +++ b/collections/requirements.yml @@ -23,3 +23,5 @@ collections: type: git source: https://github.com/fabiendupont/nvidia-ncx-infra-controller-ansible version: 1.3.0 + - name: vastdata.vms + version: 1.2.0 diff --git a/config/base/configmap-storage-operations-ig-example.yaml b/config/base/configmap-storage-operations-ig-example.yaml new file mode 100644 index 000000000..f2f6c8bcd --- /dev/null +++ b/config/base/configmap-storage-operations-ig-example.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: storage-operations-ig +data: + # Storage tier definitions — JSON array with per-tier provider selection. + # Each tier declares: name (DNS label), protocol (nfs|block), provider (vast). + # Optional: qos_policy (string) + qos_limits (dict) for VAST QoS policy creation. + # Optional: quota (int, bytes) for per-tier hard quota. + STORAGE_TIERS: | + [ + {"name": "default", "protocol": "nfs", "provider": "vast", "qos_policy": "default-qos", + "qos_limits": {"static_limits": {"max_reads_bw_mbps": 100, "max_writes_bw_mbps": 100}}}, + {"name": "high-performance", "protocol": "block", "provider": "vast", "qos_policy": "high-iops", + "qos_limits": {"static_limits": {"max_reads_bw_mbps": 500, "max_writes_bw_mbps": 500}}} + ] + + # Create VolumeSnapshotClass alongside StorageClass (requires snapshot-controller CRD). + STORAGE_SNAPSHOTS_ENABLED: "true" + + # TLS certificate validation for VAST VMS API calls. + # Set to "false" for self-signed certificates in dev/test environments. + # VAST_VALIDATE_CERTS: "true" + + # Gateway IP for cross-subnet VIP routing (optional). + # VAST_VIP_POOL_GW_IP: "" + # VAST_VIP_POOL_GW_IPV6: "" + + # Namespace on hub cluster where VAST config Secrets (vast-tenant-config-*) are stored. + # Overrides the OSAC_STORAGE_CONFIG_NAMESPACE env var injected via downward API in the pod spec. + # Only set this if the config namespace differs from the IG pod namespace. + # OSAC_STORAGE_CONFIG_NAMESPACE: "osac-system" diff --git a/config/base/secret-storage-operations-ig-example.yaml b/config/base/secret-storage-operations-ig-example.yaml new file mode 100644 index 000000000..94699c8e2 --- /dev/null +++ b/config/base/secret-storage-operations-ig-example.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Secret +metadata: + name: storage-operations-ig +type: Opaque +data: + # VAST admin credentials — used ONLY for VMS API provisioning during tenant_setup. + # These credentials are NEVER placed into K8s Secrets or CSI Secrets in tenant namespaces. + # Per-tenant data-plane credentials are generated automatically during tenant_setup. + + # Base64-encoded VAST management API URL (e.g., https://vast-vms.example.com) + VAST_ENDPOINT: "" + + # Base64-encoded VAST management API username + VAST_USERNAME: "" + + # Base64-encoded VAST management API password + VAST_PASSWORD: "" + + # Block encryption passphrase comes from the Tenant CR event payload (spec.blockEncryptionPassphrase), + # not from this Secret. It is per-tenant and passed through the EDA event. diff --git a/playbook_osac_configure_tenant_storage.yml b/playbook_osac_configure_tenant_storage.yml new file mode 100644 index 000000000..de5faeb3a --- /dev/null +++ b/playbook_osac_configure_tenant_storage.yml @@ -0,0 +1,84 @@ +--- +- name: Configure storage for a tenant + hosts: localhost + gather_facts: false + + vars: + tenant_name: "{{ ansible_eda.event.payload.metadata.name }}" + tenant_namespace: "{{ ansible_eda.event.payload.metadata.namespace }}" + _storage_tiers_raw: "{{ lookup('env', 'STORAGE_TIERS') }}" + _storage_snapshots: "{{ lookup('env', 'STORAGE_SNAPSHOTS_ENABLED') | default('true', true) | bool }}" + + pre_tasks: + - name: Show EDA Event + ansible.builtin.debug: + var: ansible_eda.event.payload + + - name: Validate tenant payload is present + ansible.builtin.fail: + msg: "tenant must be provided via ansible_eda.event.payload with metadata.name and metadata.namespace set" + when: >- + ansible_eda is not defined or + ansible_eda.event.payload.metadata.name is not defined or + ansible_eda.event.payload.metadata.name | length == 0 or + ansible_eda.event.payload.metadata.namespace is not defined or + ansible_eda.event.payload.metadata.namespace | length == 0 + + - name: Validate STORAGE_TIERS env var is configured + ansible.builtin.fail: + msg: >- + STORAGE_TIERS env var must be set at deployment time as a JSON array. + Example: [{"name":"default","protocol":"nfs","provider":"vast"}] + when: _storage_tiers_raw | length == 0 + + - name: Parse STORAGE_TIERS JSON + block: + - name: Parse tier list from env var + ansible.builtin.set_fact: + _storage_tiers: "{{ _storage_tiers_raw | from_json }}" + rescue: + - name: Fail on malformed STORAGE_TIERS JSON + ansible.builtin.fail: + msg: >- + STORAGE_TIERS env var contains invalid JSON. + Value: '{{ _storage_tiers_raw }}'. + Expected a JSON array of tier objects with name, protocol, and provider fields. + + tasks: + - name: Configure tenant storage + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_action: setup + storage_provider_tiers: "{{ _storage_tiers }}" + storage_provider_provisioning_target: vmaas + storage_provider_block_encryption_passphrase: "{{ ansible_eda.event.payload.spec.blockEncryptionPassphrase | default('') }}" + storage_provider_snapshots_enabled: "{{ _storage_snapshots }}" + + - name: Provision StorageClasses for all configured tiers + block: + - name: Create StorageClasses on target cluster + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_action: ensure_storage_class + storage_provider_tiers: "{{ _storage_tiers }}" + storage_provider_provisioning_target: vmaas + storage_provider_block_encryption_passphrase: "{{ ansible_eda.event.payload.spec.blockEncryptionPassphrase | default('') }}" + storage_provider_snapshots_enabled: "{{ _storage_snapshots }}" + rescue: + - name: Warn about StorageClass creation failure + ansible.builtin.debug: + msg: >- + StorageClass creation failed on first attempt (task: {{ ansible_failed_task.name | default('unknown') }}). + Retrying once. + + - name: Retry StorageClass creation + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_action: ensure_storage_class + storage_provider_tiers: "{{ _storage_tiers }}" + storage_provider_provisioning_target: vmaas + storage_provider_block_encryption_passphrase: "{{ ansible_eda.event.payload.spec.blockEncryptionPassphrase | default('') }}" + storage_provider_snapshots_enabled: "{{ _storage_snapshots }}" diff --git a/playbook_osac_create_compute_instance.yml b/playbook_osac_create_compute_instance.yml index daca2ada8..f791d50b0 100644 --- a/playbook_osac_create_compute_instance.yml +++ b/playbook_osac_create_compute_instance.yml @@ -9,25 +9,24 @@ template_id: "{{ ansible_eda.event.payload.spec.templateID }}" template_parameters: {} tenant_storage_classes: "{{ ansible_eda.event.tenant_storage_classes | default([]) }}" + _storage_tiers_raw: "{{ lookup('env', 'STORAGE_TIERS') }}" + _requested_storage_tier: "{{ lookup('env', 'STORAGE_REQUESTED_TIER') | default('default', true) }}" pre_tasks: - name: Show EDA Event ansible.builtin.debug: var: ansible_eda.event.payload - # Brings in variables: - # tenant_target_namespace - name: Determine tenant target namespace + # Brings in variables: + # tenant_target_namespace ansible.builtin.include_role: name: osac.service.tenant_target_namespace - - name: Fail if tenant storage classes not injected - ansible.builtin.fail: - msg: >- - ComputeInstance '{{ compute_instance_name }}' has no tenant_storage_classes - in extra_vars. The osac-operator CI controller should inject the resolved - storageClasses list before triggering provisioning. - when: tenant_storage_classes | length == 0 + - name: Set tenant identity from ComputeInstance tenant reference + ansible.builtin.set_fact: + tenant_name: "{{ compute_instance.status.tenantReference.name }}" + tenant_namespace: "{{ compute_instance.status.tenantReference.namespace }}" - name: Set storage classes context for downstream roles ansible.builtin.set_fact: @@ -39,8 +38,63 @@ else (compute_instance.metadata.annotations | default({})).get('osac.openshift.io/tenant', 'unknown') }} - tasks: + - name: Parse STORAGE_TIERS for JIT storage + when: _storage_tiers_raw | length > 0 + block: + - name: Parse tier list from STORAGE_TIERS env var + ansible.builtin.set_fact: + _jit_all_tiers: "{{ _storage_tiers_raw | from_json }}" + - name: Filter to only the requested tier + ansible.builtin.set_fact: + _jit_storage_tiers: "{{ _jit_all_tiers | selectattr('name', 'equalto', _requested_storage_tier) | list }}" + + - name: Warn if requested tier not found in STORAGE_TIERS + ansible.builtin.debug: + msg: >- + Requested storage tier '{{ _requested_storage_tier }}' not found in STORAGE_TIERS. + Available tiers: {{ _jit_all_tiers | map(attribute='name') | list | to_json }}. + JIT storage provisioning will be skipped. + when: _jit_storage_tiers | length == 0 + rescue: + - name: Fail on malformed STORAGE_TIERS JSON + ansible.builtin.fail: + msg: >- + STORAGE_TIERS env var contains invalid JSON. + Value: '{{ _storage_tiers_raw }}'. + Expected a JSON array of tier objects with name, protocol, and provider fields. + + - name: Ensure storage is provisioned for tenant (JIT) + when: _jit_storage_tiers is defined and _jit_storage_tiers | length > 0 + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_action: ensure_storage_class + storage_provider_tiers: "{{ _jit_storage_tiers }}" + storage_provider_provisioning_target: vmaas + storage_provider_block_encryption_passphrase: "{{ ansible_eda.event.payload.spec.blockEncryptionPassphrase | default('') }}" + storage_provider_snapshots_enabled: "{{ lookup('env', 'STORAGE_SNAPSHOTS_ENABLED') | default('true', true) | bool }}" + + - name: Add JIT-provisioned StorageClasses to resolution list + ansible.builtin.set_fact: + tenant_storage_class_storage_classes: >- + {{ tenant_storage_class_storage_classes + (storage_provider_storage_class_names | default([])) }} + when: + - storage_provider_storage_class_names is defined + - storage_provider_storage_class_names | length > 0 + + - name: Fail if tenant storage classes not available + ansible.builtin.fail: + msg: >- + ComputeInstance '{{ compute_instance_name }}' has no tenant_storage_classes + available. Either the osac-operator CI controller should inject the resolved + storageClasses list before triggering provisioning, or JIT storage provisioning + via STORAGE_TIERS must succeed. + when: + - tenant_storage_class_storage_classes | length == 0 + - _jit_storage_tiers is not defined or _jit_storage_tiers | length == 0 + + tasks: - name: Display compute instance information ansible.builtin.debug: msg: diff --git a/playbook_osac_delete_tenant_storage.yml b/playbook_osac_delete_tenant_storage.yml new file mode 100644 index 000000000..967385cbf --- /dev/null +++ b/playbook_osac_delete_tenant_storage.yml @@ -0,0 +1,53 @@ +--- +- name: Delete storage for a tenant + hosts: localhost + gather_facts: false + + vars: + tenant_name: "{{ ansible_eda.event.payload.metadata.name }}" + tenant_namespace: "{{ ansible_eda.event.payload.metadata.namespace }}" + _storage_tiers_raw: "{{ lookup('env', 'STORAGE_TIERS') }}" + + pre_tasks: + - name: Show EDA Event + ansible.builtin.debug: + var: ansible_eda.event.payload + + - name: Validate tenant payload is present + ansible.builtin.fail: + msg: "tenant must be provided via ansible_eda.event.payload with metadata.name and metadata.namespace set" + when: >- + ansible_eda is not defined or + ansible_eda.event.payload.metadata.name is not defined or + ansible_eda.event.payload.metadata.name | length == 0 or + ansible_eda.event.payload.metadata.namespace is not defined or + ansible_eda.event.payload.metadata.namespace | length == 0 + + - name: Parse STORAGE_TIERS JSON + when: _storage_tiers_raw | length > 0 + block: + - name: Parse tier list from env var + ansible.builtin.set_fact: + _storage_tiers: "{{ _storage_tiers_raw | from_json }}" + rescue: + - name: Fail on malformed STORAGE_TIERS JSON + ansible.builtin.fail: + msg: >- + STORAGE_TIERS env var contains invalid JSON. + Value: '{{ _storage_tiers_raw }}'. + Expected a JSON array of tier objects with name, protocol, and provider fields. + + tasks: + - name: Skip teardown when STORAGE_TIERS is not configured + when: _storage_tiers_raw | length == 0 + ansible.builtin.debug: + msg: "STORAGE_TIERS env var is not set — nothing to tear down." + + - name: Delete tenant storage + when: _storage_tiers_raw | length > 0 + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_action: teardown + storage_provider_tiers: "{{ _storage_tiers }}" + storage_provider_provisioning_target: vmaas diff --git a/tests/integration/fixtures/storage/secret-vast-tenant-config-test.yaml b/tests/integration/fixtures/storage/secret-vast-tenant-config-test.yaml new file mode 100644 index 000000000..20f0924c8 --- /dev/null +++ b/tests/integration/fixtures/storage/secret-vast-tenant-config-test.yaml @@ -0,0 +1,22 @@ +--- +# WARNING: This file contains PLACEHOLDER values for testing only. +# NEVER insert real credentials into this file. +apiVersion: v1 +kind: Secret +metadata: + name: vast-tenant-config-test-tenant + namespace: osac-system +type: Opaque +stringData: + vast_tenant_id: "PLACEHOLDER_DO_NOT_USE_999" + vast_local_provider_id: "PLACEHOLDER_DO_NOT_USE_1" + vip_pool_name: "PLACEHOLDER_DO_NOT_USE_vippool-test-tenant" + vast_endpoint: "PLACEHOLDER_DO_NOT_USE_https://vast-vms.example.com" + storage_tiers: '[{"name":"default","protocol":"nfs","provider":"vast","qos_policy":"test-qos"}]' + storage_provider_type: "vast" + tenant_manager_name: "PLACEHOLDER_DO_NOT_USE_osac-test-tenant" + tenant_manager_username: "PLACEHOLDER_DO_NOT_USE_osac-test-tenant" + tenant_manager_password: "PLACEHOLDER_DO_NOT_USE_password-placeholder" + tenant_manager_id: "PLACEHOLDER_DO_NOT_USE_42" + tenant_role_id: "PLACEHOLDER_DO_NOT_USE_7" + view_policy_name: "PLACEHOLDER_DO_NOT_USE_osac-test-tenant" diff --git a/tests/integration/fixtures/storage/storageclass-vast-test.yaml b/tests/integration/fixtures/storage/storageclass-vast-test.yaml new file mode 100644 index 000000000..adff97b0a --- /dev/null +++ b/tests/integration/fixtures/storage/storageclass-vast-test.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: vast-nfs-test-tenant-default + labels: + app.kubernetes.io/managed-by: osac-aap + osac.openshift.io/tenant: test-tenant + osac.openshift.io/storage-tier: default + osac.openshift.io/storage-protocol: nfs +provisioner: csi.vastdata.com +parameters: + vip_pool_name: vippool-test-tenant + root_export: /osac/test-tenant/default + csi.storage.k8s.io/provisioner-secret-name: vast-csi-test-tenant + csi.storage.k8s.io/provisioner-secret-namespace: test-tenant-ns + csi.storage.k8s.io/controller-publish-secret-name: vast-csi-test-tenant + csi.storage.k8s.io/controller-publish-secret-namespace: test-tenant-ns + csi.storage.k8s.io/node-publish-secret-name: vast-csi-test-tenant + csi.storage.k8s.io/node-publish-secret-namespace: test-tenant-ns + csi.storage.k8s.io/controller-expand-secret-name: vast-csi-test-tenant + csi.storage.k8s.io/controller-expand-secret-namespace: test-tenant-ns + csi.storage.k8s.io/node-expand-secret-name: vast-csi-test-tenant + csi.storage.k8s.io/node-expand-secret-namespace: test-tenant-ns +reclaimPolicy: Delete +volumeBindingMode: Immediate diff --git a/tests/integration/fixtures/storage/tenant-storage-test.yaml b/tests/integration/fixtures/storage/tenant-storage-test.yaml new file mode 100644 index 000000000..1ad92a654 --- /dev/null +++ b/tests/integration/fixtures/storage/tenant-storage-test.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: osac.openshift.io/v1alpha1 +kind: Tenant +metadata: + name: test-tenant + namespace: osac-system +spec: {} diff --git a/tests/integration/integration_config.yml.template b/tests/integration/integration_config.yml.template new file mode 100644 index 000000000..5ca357dda --- /dev/null +++ b/tests/integration/integration_config.yml.template @@ -0,0 +1,20 @@ +# VAST VMS credentials for live API integration tests (Tier 3) +# Copy to integration_config.yml and fill in values, or render from CI: +# envsubst < integration_config.yml.template > integration_config.yml +# +# WARNING: NEVER commit integration_config.yml with real credentials. +vast_endpoint: "${VAST_ENDPOINT}" +vast_username: "${VAST_USERNAME}" +vast_password: "${VAST_PASSWORD}" +vast_tenant_prefix: "osac-test-" +# TLS validation — set to false for mock server or self-signed cert testing +vast_validate_certs: ${VAST_VALIDATE_CERTS:-true} + +# Shared VIP pool configuration +vast_vip_pool_name: "${VAST_VIP_POOL_NAME:-osac-shared}" +vast_vip_pool_ip_ranges: '${VAST_VIP_POOL_IP_RANGES:-[["10.0.100.10","10.0.100.50"]]}' +vast_vip_pool_subnet_cidr: ${VAST_VIP_POOL_SUBNET_CIDR:-24} + +# Storage tier definition — JSON array with per-tier provider selection. +# Used by test playbooks to exercise the multi-tier storage provider interface. +storage_tiers: '${STORAGE_TIERS}' diff --git a/tests/integration/mock_vms_server.py b/tests/integration/mock_vms_server.py new file mode 100644 index 000000000..c8960ccb7 --- /dev/null +++ b/tests/integration/mock_vms_server.py @@ -0,0 +1,348 @@ +"""Mock VAST VMS API server for storage provider integration tests. + +Simulates VAST VMS REST endpoints for tenants, vippools, views, +viewpolicies, quotas, roles, managers, and apitokens. +Follows the mock_api_server.py pattern. + +Auth: vast-ansible sends either ``Authorization: Api-Token `` header, +``Authorization: Bearer `` header, or HTTP Basic Auth on every +request. ``POST /api/token/`` returns a mock bearer token. + +Usage: + python3 mock_vms_server.py [--tls --cert --key ] +""" + +import argparse +import copy +import json +import ssl +import threading +from http.server import BaseHTTPRequestHandler, HTTPServer +from socketserver import ThreadingMixIn +from urllib.parse import parse_qs, urlparse + +CALL_LOG = [] +_INJECTED_FAILURES = [] + +_NEXT_ID = {r: 1 for r in ( + "tenants", "vippools", "views", "viewpolicies", "quotas", "qospolicies", + "users", "roles", "managers", "apitokens", "localproviders", +)} +_STORE = {r: {} for r in _NEXT_ID} +_LOCK = threading.Lock() + +CANNED_DEFAULTS = { + "tenants": {"name": "", "client_ip_ranges": [], "encryption": False, "local_provider_id": 1}, + "vippools": {"name": "", "ip_ranges": [], "tenant_id": 1}, + "views": {"name": "", "path": "/", "policy_id": 1, "tenant_id": 1}, + "viewpolicies": {"name": "", "flavor": "NFS", "protocols": ["NFS"], "tenant_id": 1}, + "quotas": {"name": "", "hard_limit": 0, "soft_limit": 0, "tenant_id": 1}, + "qospolicies": {"name": "", "tenant_id": 1, "mode": "STATIC"}, + "users": {"name": "", "local_provider_id": 1}, + "roles": {"name": "", "tenant_id": 1, "tenant_ids": [], "permissions_list": []}, + "managers": {"username": "", "user_type": "TENANT_ADMIN", "tenant_id": 1, "roles": []}, + "apitokens": {"owner": "", "name": "", "token": ""}, + "localproviders": {"name": "", "managed_by": [], "description": ""}, +} + +# Query parameters supported for filtering list responses. +# Each resource maps to a list of field names that can be used as query params. +_LIST_FILTER_FIELDS = { + "tenants": ["name"], + "vippools": ["name"], + "views": ["path", "tenant_id"], + "quotas": ["path", "tenant_id"], + "roles": ["name", "tenant_id"], + "managers": ["username"], + "apitokens": ["owner"], + "viewpolicies": ["name", "tenant_id"], + "qospolicies": ["name", "tenant_id"], + "localproviders": ["name"], +} + +_RESOURCES = set(CANNED_DEFAULTS) + + +def _strip_sensitive(headers): + """Return header dict with Authorization removed.""" + return {k: v for k, v in headers.items() if k.lower() != "authorization"} + + +def _log(entry): + if "body" in entry and isinstance(entry["body"], dict): + entry = dict(entry) + body = dict(entry["body"]) + for field in ("password", "secret", "token"): + body.pop(field, None) + entry["body"] = body + with _LOCK: + CALL_LOG.append(entry) + + +class MockVmsHandler(BaseHTTPRequestHandler): + def _check_injected_failure(self, method): + """Check if an injected failure matches this request. Returns (status, body) or None.""" + resource, _ = self._parse_path() + with _LOCK: + for i, f in enumerate(_INJECTED_FAILURES): + if f["resource"] == resource and f["method"] == method: + status = f.get("status", 500) + body = f.get("body", {"error": "injected failure"}) + _INJECTED_FAILURES.pop(i) + return status, body + return None + + def _parse_path(self): + """Return (resource, resource_id) or (None, None) for non-resource paths.""" + path = self.path.split("?")[0].rstrip("/") + parts = path.strip("/").split("/") + if len(parts) >= 2 and parts[0] == "api": + if len(parts) >= 3 and parts[1] not in _RESOURCES: + parts = parts[2:] + else: + parts = parts[1:] + if not parts or parts[0] not in _RESOURCES: + return None, None + resource = parts[0] + resource_id = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else None + return resource, resource_id + + def _read_body(self): + length = int(self.headers.get("Content-Length", 0)) + if not length: + return {} + try: + return json.loads(self.rfile.read(length)) + except (json.JSONDecodeError, ValueError): + return None + + def _respond(self, status, data): + body = json.dumps(data).encode() + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + path = self.path.split("?")[0] + + if path == "/_calls": + with _LOCK: + snapshot = list(CALL_LOG) + self._respond(200, snapshot) + return + + if path == "/_reset": + with _LOCK: + CALL_LOG.clear() + for r in _STORE: + _STORE[r].clear() + for r in _NEXT_ID: + _NEXT_ID[r] = 1 + self._respond(200, {"status": "reset"}) + return + + if path.rstrip("/") == "/api": + _log({"method": "GET", "path": path, "headers": _strip_sensitive(dict(self.headers))}) + self._respond(200, {"status": "ok"}) + return + + if path.rstrip("/").endswith("/clusters"): + _log({"method": "GET", "path": path, "headers": _strip_sensitive(dict(self.headers))}) + self._respond(200, [{"id": 1, "name": "mock-cluster", "sw_version": "5.4.0"}]) + return + + resource, resource_id = self._parse_path() + _log({"method": "GET", "path": self.path, "headers": _strip_sensitive(dict(self.headers))}) + + if resource is None: + self._respond(404, {"error": "not found"}) + return + + with _LOCK: + if resource_id is not None: + obj = _STORE[resource].get(resource_id) + else: + obj = list(_STORE[resource].values()) + + # Apply query-parameter filtering (supports multiple fields) + filter_fields = _LIST_FILTER_FIELDS.get(resource, []) + if filter_fields: + qs = parse_qs(urlparse(self.path).query) + for field in filter_fields: + filter_value = qs.get(field, [None])[0] + if filter_value is not None: + obj = [o for o in obj if str(o.get(field, "")) == filter_value] + + if resource_id is not None: + if obj is None: + self._respond(404, {"error": f"{resource} {resource_id} not found"}) + else: + self._respond(200, obj) + else: + self._respond(200, obj) + + def do_POST(self): + path = self.path.split("?")[0] + + if path == "/_reset": + with _LOCK: + CALL_LOG.clear() + _INJECTED_FAILURES.clear() + for r in _STORE: + _STORE[r].clear() + for r in _NEXT_ID: + _NEXT_ID[r] = 1 + self._respond(200, {"status": "reset"}) + return + + if path == "/_inject_failure": + body = self._read_body() + if not body or "resource" not in body or "method" not in body: + self._respond(400, {"error": "resource and method required"}) + return + with _LOCK: + _INJECTED_FAILURES.append(body) + self._respond(200, {"status": "failure injected", "pending": len(_INJECTED_FAILURES)}) + return + + body = self._read_body() + if body is None: + self._respond(400, {"error": "invalid JSON body"}) + return + _log({"method": "POST", "path": path, "headers": _strip_sensitive(dict(self.headers)), "body": body}) + + # Special endpoint: /api/token/ — authentication (no resource CRUD) + if path.rstrip("/") == "/api/token": + self._respond(200, {"access": "mock-bearer-token-XXXXXX"}) + return + + failure = self._check_injected_failure("POST") + if failure: + self._respond(failure[0], failure[1]) + return + + resource, _ = self._parse_path() + if resource is None: + self._respond(404, {"error": "not found"}) + return + + # QoS STATIC mode requires at least one limit field + if resource == "qospolicies": + mode = body.get("mode", "") + if mode == "STATIC" and not body.get("static_limits") and not body.get("static_total_limits"): + self._respond(400, { + "error": "At least one Qos Static or Static Total Limit should be set" + }) + return + + with _LOCK: + # Cross-tenant view policy validation + if resource == "views": + policy_id = body.get("policy_id") + view_tenant = body.get("tenant_id", 1) + if policy_id is not None: + policy = _STORE["viewpolicies"].get(int(policy_id)) + if policy and int(policy.get("tenant_id", 1)) != int(view_tenant): + self._respond(400, { + "error": "You cannot attach a view policy that belongs to a different tenant." + }) + return + + obj_id = _NEXT_ID[resource] + _NEXT_ID[resource] += 1 + obj = {**copy.deepcopy(CANNED_DEFAULTS[resource]), **body, "id": obj_id} + + # Resource-specific post-processing + if resource == "managers": + obj.pop("password", None) + elif resource == "apitokens": + obj["token"] = f"mock-api-token-{obj_id}" + elif resource == "tenants": + lp_id = _NEXT_ID["localproviders"] + _NEXT_ID["localproviders"] += 1 + lp = { + "id": lp_id, + "name": f"provider-{obj.get('name', '')}", + "tenant_id": obj_id, + "managed_by": [], + "description": "", + } + _STORE["localproviders"][lp_id] = lp + obj["local_provider_id"] = lp_id + + _STORE[resource][obj_id] = obj + self._respond(201, obj) + + def do_PATCH(self): + path = self.path.split("?")[0] + body = self._read_body() + if body is None: + self._respond(400, {"error": "invalid JSON body"}) + return + _log({"method": "PATCH", "path": path, "headers": _strip_sensitive(dict(self.headers)), "body": body}) + + resource, resource_id = self._parse_path() + if resource is None or resource_id is None: + self._respond(404, {"error": "not found"}) + return + + result = None + with _LOCK: + obj = _STORE[resource].get(resource_id) + if obj is not None: + obj.update(body) + result = dict(obj) + if result is None: + self._respond(404, {"error": f"{resource} {resource_id} not found"}) + else: + self._respond(200, result) + + def do_DELETE(self): + path = self.path.split("?")[0] + _log({"method": "DELETE", "path": path, "headers": _strip_sensitive(dict(self.headers))}) + + resource, resource_id = self._parse_path() + if resource is None or resource_id is None: + self._respond(404, {"error": "not found"}) + return + + with _LOCK: + if resource_id not in _STORE[resource]: + found = False + else: + found = True + del _STORE[resource][resource_id] + if not found: + self._respond(404, {"error": f"{resource} {resource_id} not found"}) + else: + self.send_response(204) + self.end_headers() + + def log_message(self, format, *args): + pass + + +class ThreadingHTTPServer(ThreadingMixIn, HTTPServer): + daemon_threads = True + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("port", type=int) + parser.add_argument("--tls", action="store_true") + parser.add_argument("--cert", default=None) + parser.add_argument("--key", default=None) + args = parser.parse_args() + + ThreadingHTTPServer.allow_reuse_address = True + server = ThreadingHTTPServer(("127.0.0.1", args.port), MockVmsHandler) + + if args.tls: + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ctx.load_cert_chain(certfile=args.cert, keyfile=args.key) + server.socket = ctx.wrap_socket(server.socket, server_side=True) + + print(f"Mock VMS server running on port {args.port} (tls={args.tls})", flush=True) + server.serve_forever() diff --git a/tests/integration/run_tests.sh b/tests/integration/run_tests.sh index be046309a..f2e7d1863 100755 --- a/tests/integration/run_tests.sh +++ b/tests/integration/run_tests.sh @@ -150,6 +150,47 @@ done # Clean up lease test pod kubectl delete pod lease-test-pod -n osac-system --ignore-not-found 2>/dev/null || true +# Storage provider tests (conditional) +if [ "${STORAGE_TESTS_ENABLED:-}" = "true" ]; then + # Source env vars written by setup_test_env.sh (Make runs each recipe line in a separate shell) + if [ -f "${SCRIPT_DIR}/.storage_env" ]; then + # shellcheck source=/dev/null + . "${SCRIPT_DIR}/.storage_env" + fi + echo "=== Running Storage Provider Tests ===" + echo "" + + # Reset mock server once before parallel tests (individual tests no longer reset) + curl -sk -X POST https://127.0.0.1:18443/_reset > /dev/null 2>&1 || true + + # Storage tests share a mock VMS server with a global call log and object + # store. Tests that assert on the call log or pre-seed VMS resources cannot + # run in parallel without cross-contamination. Run all sequentially — each + # test takes ~7s so the total overhead is negligible. + STORAGE_TESTS=( + "storage_provider_setup" + "storage_provider_teardown" + "storage_provider_ensure_sc" + "storage_provider_onboarding" + "storage_provider_setup_rollback" + ) + + for storage_test in "${STORAGE_TESTS[@]}"; do + echo " Running: $storage_test" + log_file="/tmp/osac_storage_test_${storage_test}.log" + if ansible-playbook "targets/${storage_test}/tasks/main.yml" -e "@common_vars.yml" -v > "${log_file}" 2>&1; then + echo " ✓ ${storage_test} passed" + PASSED+=("$storage_test:baseline") + else + echo " ✗ ${storage_test} failed (see ${log_file})" + echo " --- ${storage_test} failure log (last 60 lines) ---" + tail -60 "${log_file}" 2>/dev/null || true + echo " --- end ${storage_test} failure log ---" + FAILED+=("$storage_test:baseline") + fi + done +fi + echo "========================================" echo "Test Results" echo "========================================" diff --git a/tests/integration/setup_test_env.sh b/tests/integration/setup_test_env.sh index 4a66ad537..9e2777825 100755 --- a/tests/integration/setup_test_env.sh +++ b/tests/integration/setup_test_env.sh @@ -8,19 +8,19 @@ echo "=== Setting up test environment ===" # 0. Delete existing cluster if it exists echo "Cleaning up any existing test cluster..." -KIND_EXPERIMENTAL_PROVIDER=podman kind delete cluster --name osac-test 2>/dev/null || true +kind delete cluster --name osac-test 2>/dev/null || true # 0.5. Install required Python libraries echo "Installing required Python libraries..." -pip install --user kubernetes openstacksdk 2>/dev/null || pip3 install --user kubernetes openstacksdk +pip install kubernetes openstacksdk 2>/dev/null || uv pip install kubernetes openstacksdk 2>/dev/null || uv pip install --system kubernetes openstacksdk # 1. Create kind cluster echo "Creating kind cluster..." -KIND_EXPERIMENTAL_PROVIDER=podman kind create cluster --name osac-test --wait 5m +kind create cluster --name osac-test --wait 5m # 1.5. Export kubeconfig to dedicated file echo "Exporting kubeconfig to dedicated file..." -KIND_EXPERIMENTAL_PROVIDER=podman kind export kubeconfig --name osac-test --kubeconfig "${SCRIPT_DIR}/kubeconfig-osac-test" +kind export kubeconfig --name osac-test --kubeconfig "${SCRIPT_DIR}/kubeconfig-osac-test" echo "Kubeconfig exported to: ${SCRIPT_DIR}/kubeconfig-osac-test" # 2. Clone osac-operator for CRDs @@ -110,4 +110,71 @@ kubectl apply -f "${SCRIPT_DIR}/fixtures/clusterorder-test.yaml" kubectl apply -f "${SCRIPT_DIR}/fixtures/computeinstance-test.yaml" kubectl apply -f "${SCRIPT_DIR}/fixtures/computeinstance-with-gpu-test.yaml" +# 6. Apply storage test fixtures and CRDs (conditional) +if [ "${STORAGE_TESTS_ENABLED:-}" = "true" ]; then + echo "Installing VolumeSnapshot CRDs for storage tests..." + kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/v8.5.0/client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml 2>/dev/null || echo "VolumeSnapshotClass CRD may already exist" + kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/v8.5.0/client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml 2>/dev/null || echo "VolumeSnapshot CRD may already exist" + kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/v8.5.0/client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml 2>/dev/null || echo "VolumeSnapshotContent CRD may already exist" + + echo "Applying storage test fixtures..." + kubectl apply -f "${SCRIPT_DIR}/fixtures/storage/" || true + + echo "Creating fake VAST CSIDrivers (short-circuits OLM installation in tests)..." + kubectl apply -f - </dev/null + + # Start mock VMS server for storage integration tests (TLS required by vastdata.vms modules) + echo "Starting mock VMS server (TLS)..." + python3 "${SCRIPT_DIR}/mock_vms_server.py" 18443 \ + --tls --cert "${SCRIPT_DIR}/certs/mock.pem" --key "${SCRIPT_DIR}/certs/mock.key" & + MOCK_VMS_PID=$! + echo "${MOCK_VMS_PID}" > "${SCRIPT_DIR}/.mock_vms_pid" + + # Wait for mock server to be ready + for i in $(seq 1 10); do + if curl -sk https://127.0.0.1:18443/api > /dev/null 2>&1; then + echo "Mock VMS server ready on port 18443 (PID: ${MOCK_VMS_PID})" + break + fi + sleep 1 + done + + # Create storage test namespace and ConfigMap + kubectl create namespace test-tenant-ns || true + + # Write VAST env vars to file for run_tests.sh (Make runs each recipe line in a separate shell) + cat > "${SCRIPT_DIR}/.storage_env" <<'ENVEOF' +export VAST_ENDPOINT="127.0.0.1:18443" +export VAST_USERNAME="admin" +export VAST_PASSWORD="admin" +export VAST_VIP_POOL_NAME="osac-test-pool" +export VAST_VIP_POOL_IP_RANGES='[["10.0.0.10","10.0.0.50"]]' +export VAST_VIP_POOL_SUBNET_CIDR="24" +export VAST_VALIDATE_CERTS="false" +export STORAGE_TIERS='[{"name":"default","protocol":"nfs","provider":"vast","qos_policy":"test-qos","qos_limits":{"static_limits":{"max_reads_bw_mbps":100,"max_writes_bw_mbps":100}}}]' +ENVEOF +fi + echo "=== Test environment ready ===" diff --git a/tests/integration/targets/compute_instance_create/tasks/baseline.yml b/tests/integration/targets/compute_instance_create/tasks/baseline.yml index c9dc4d376..29dee7004 100644 --- a/tests/integration/targets/compute_instance_create/tasks/baseline.yml +++ b/tests/integration/targets/compute_instance_create/tasks/baseline.yml @@ -16,6 +16,7 @@ event: payload: "{{ test_compute_instance }}" tenant_target_namespace: "computeinstance-test-vm-work" + tenant_storage_class_name: "test-storage-class" # Override only resource creation to prevent actual VM/DataVolume creation create_step_resources_override: name: osac.workflows.workflow_helpers diff --git a/tests/integration/targets/compute_instance_create/tasks/overrides.yml b/tests/integration/targets/compute_instance_create/tasks/overrides.yml index f23421793..ef9cba06b 100644 --- a/tests/integration/targets/compute_instance_create/tasks/overrides.yml +++ b/tests/integration/targets/compute_instance_create/tasks/overrides.yml @@ -26,6 +26,7 @@ event: payload: "{{ test_compute_instance }}" tenant_target_namespace: "computeinstance-test-vm-work" + tenant_storage_class_name: "test-storage-class" # Override 3 workflow extension points hook_workflow_start: diff --git a/tests/integration/targets/compute_instance_with_gpu_create/tasks/baseline.yml b/tests/integration/targets/compute_instance_with_gpu_create/tasks/baseline.yml index 9c2fbd8f3..a01b0f610 100644 --- a/tests/integration/targets/compute_instance_with_gpu_create/tasks/baseline.yml +++ b/tests/integration/targets/compute_instance_with_gpu_create/tasks/baseline.yml @@ -16,6 +16,7 @@ event: payload: "{{ test_compute_instance }}" tenant_target_namespace: "computeinstance-test-vm-gpu-work" + tenant_storage_class_name: "test-storage-class" create_step_resources_override: name: osac.workflows.workflow_helpers tasks_from: noop.yml diff --git a/tests/integration/targets/storage_provider_ensure_sc/tasks/main.yml b/tests/integration/targets/storage_provider_ensure_sc/tasks/main.yml new file mode 100644 index 000000000..98e388653 --- /dev/null +++ b/tests/integration/targets/storage_provider_ensure_sc/tasks/main.yml @@ -0,0 +1,463 @@ +--- +# Integration test for osac.service.storage_provider ensure_storage_class action. +# Tests StorageClass, CSI Secret, and VolumeSnapshotClass provisioning. +# Requires: kind cluster with osac-system namespace and VolumeSnapshot CRDs. + +# ── Setup: Pre-create hub Secret fixture ── +- name: "Storage Provider Ensure SC - Setup fixtures" + hosts: localhost + gather_facts: false + + tasks: + - name: Delete any pre-existing test resources (clean slate) + block: + - name: Delete hub Secret if exists + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "vast-tenant-config-test-ensuresc" + namespace: "osac-system" + failed_when: false + + - name: Delete CSI Secret if exists + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "vast-csi-test-ensuresc" + namespace: "osac-system" + failed_when: false + + - name: Delete NFS StorageClass if exists + kubernetes.core.k8s: + state: absent + api_version: storage.k8s.io/v1 + kind: StorageClass + name: "vast-nfs-test-ensuresc-default" + failed_when: false + + - name: Delete block StorageClass if exists + kubernetes.core.k8s: + state: absent + api_version: storage.k8s.io/v1 + kind: StorageClass + name: "vast-block-test-ensuresc-block-tier" + failed_when: false + + - name: Delete VolumeSnapshotClass if exists + kubernetes.core.k8s: + state: absent + api_version: snapshot.storage.k8s.io/v1 + kind: VolumeSnapshotClass + name: "vast-snapshot-test-ensuresc-default" + failed_when: false + + - name: Create hub Secret fixture (simulates output from setup action) + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Secret + metadata: + name: "vast-tenant-config-test-ensuresc" + namespace: "osac-system" + labels: + app.kubernetes.io/managed-by: osac-aap + osac.openshift.io/tenant: "test-ensuresc" + type: Opaque + stringData: + vast_tenant_id: "999" + vast_local_provider_id: "1" + vip_pool_name: "osac-test-pool" + vast_endpoint: "127.0.0.1:18443" + storage_tiers: '[{"name":"default","protocol":"nfs","provider":"vast","qos_policy":"test-qos"},{"name":"block-tier","protocol":"block","provider":"vast"}]' + storage_provider_type: "vast" + tenant_manager_name: "osac-test-ensuresc" + tenant_manager_username: "osac-test-ensuresc" + tenant_manager_password: "test-csi-password" + tenant_manager_id: "42" + tenant_role_id: "7" + view_policy_name: "osac-test-ensuresc-nfs" + view_policy_names: '{"nfs":"osac-test-ensuresc-nfs","block":"osac-test-ensuresc-block"}' + +# ── Test: Ensure StorageClass provisioning ── +- name: "Storage Provider Ensure SC - Run ensure_storage_class" + hosts: localhost + gather_facts: false + + environment: + VAST_ENDPOINT: "127.0.0.1:18443" + VAST_USERNAME: "admin" + VAST_PASSWORD: "admin" + OSAC_STORAGE_CONFIG_NAMESPACE: "osac-system" + + vars: + tenant_name: "test-ensuresc" + tenant_namespace: "osac-system" + storage_provider_tiers: + - name: default + protocol: nfs + provider: vast + - name: block-tier + protocol: block + provider: vast + storage_provider_action: ensure_storage_class + storage_provider_provisioning_target: vmaas + storage_provider_snapshots_enabled: true + storage_provider_block_encryption_passphrase: "test-encryption-passphrase" + vast_storage_validate_certs: false + + tasks: + - name: Call storage_provider role with ensure_storage_class action + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_action: ensure_storage_class + +# ── Verify: NFS StorageClass, Block StorageClass, CSI Secret, VolumeSnapshotClass ── +- name: "Storage Provider Ensure SC - Verify StorageClass" + hosts: localhost + gather_facts: false + + vars: + tenant_name: "test-ensuresc" + + tasks: + - name: Read StorageClass + kubernetes.core.k8s_info: + api_version: storage.k8s.io/v1 + kind: StorageClass + name: "vast-nfs-test-ensuresc-default" + register: _sc_result + + - name: Assert StorageClass exists + ansible.builtin.assert: + that: + - _sc_result.resources | length == 1 + fail_msg: "StorageClass 'vast-nfs-test-ensuresc-default' was not created" + success_msg: "StorageClass exists" + + - name: Assert StorageClass provisioner + ansible.builtin.assert: + that: + - _sc_result.resources[0].provisioner == "csi.vastdata.com" + fail_msg: "Expected provisioner 'csi.vastdata.com', got '{{ _sc_result.resources[0].provisioner }}'" + success_msg: "StorageClass provisioner correct" + + - name: Assert StorageClass parameters + ansible.builtin.assert: + that: + - _sc_result.resources[0].parameters.vip_pool_name == "osac-test-pool" + - _sc_result.resources[0].parameters.root_export == "/osac/test-ensuresc/default" + - "_sc_result.resources[0].parameters['csi.storage.k8s.io/provisioner-secret-name'] == 'vast-csi-test-ensuresc'" + - "_sc_result.resources[0].parameters['csi.storage.k8s.io/provisioner-secret-namespace'] == 'osac-system'" + - _sc_result.resources[0].parameters.view_policy == "osac-test-ensuresc-nfs" + fail_msg: "StorageClass parameters incorrect: {{ _sc_result.resources[0].parameters }}" + success_msg: "StorageClass parameters correct" + + - name: Assert NFS StorageClass does NOT have encryption parameter + ansible.builtin.assert: + that: + - _sc_result.resources[0].parameters.encryption is not defined + - _sc_result.resources[0].parameters.hostEncryption is not defined + fail_msg: "NFS StorageClass should not have encryption parameters" + success_msg: "NFS StorageClass correctly excludes encryption parameters" + + - name: Assert StorageClass labels + ansible.builtin.assert: + that: + - _sc_result.resources[0].metadata.labels['app.kubernetes.io/managed-by'] == 'osac-aap' + - _sc_result.resources[0].metadata.labels['osac.openshift.io/tenant'] == 'test-ensuresc' + - _sc_result.resources[0].metadata.labels['osac.openshift.io/storage-tier'] == 'default' + - _sc_result.resources[0].metadata.labels['osac.openshift.io/storage-protocol'] == 'nfs' + fail_msg: "StorageClass labels incorrect" + success_msg: "StorageClass labels correct" + + - name: Assert NFS StorageClass reclaim policy and binding mode + ansible.builtin.assert: + that: + - _sc_result.resources[0].reclaimPolicy == "Delete" + - _sc_result.resources[0].volumeBindingMode == "Immediate" + fail_msg: "NFS StorageClass reclaimPolicy or volumeBindingMode incorrect" + success_msg: "NFS StorageClass reclaimPolicy and volumeBindingMode correct" + + # ── Verify block StorageClass ── + - name: Read block StorageClass + kubernetes.core.k8s_info: + api_version: storage.k8s.io/v1 + kind: StorageClass + name: "vast-block-test-ensuresc-block-tier" + register: _block_sc_result + + - name: Assert block StorageClass exists + ansible.builtin.assert: + that: + - _block_sc_result.resources | length == 1 + fail_msg: "StorageClass 'vast-block-test-ensuresc-block-tier' was not created" + success_msg: "Block StorageClass exists" + + - name: Assert block StorageClass provisioner + ansible.builtin.assert: + that: + - _block_sc_result.resources[0].provisioner == "block.csi.vastdata.com" + fail_msg: "Expected provisioner 'block.csi.vastdata.com', got '{{ _block_sc_result.resources[0].provisioner }}'" + success_msg: "Block StorageClass provisioner correct" + + - name: Assert block StorageClass uses WaitForFirstConsumer binding mode + ansible.builtin.assert: + that: + - _block_sc_result.resources[0].volumeBindingMode == "WaitForFirstConsumer" + fail_msg: "Block StorageClass should use WaitForFirstConsumer, got '{{ _block_sc_result.resources[0].volumeBindingMode }}'" + success_msg: "Block StorageClass volumeBindingMode correct" + + - name: Assert block StorageClass has host_encryption parameter when passphrase provided + ansible.builtin.assert: + that: + - _block_sc_result.resources[0].parameters.host_encryption is defined + fail_msg: "Block StorageClass should have host_encryption when passphrase is provided" + success_msg: "Block StorageClass host_encryption parameter correct" + + - name: Assert block StorageClass parameters + ansible.builtin.assert: + that: + - _block_sc_result.resources[0].parameters.vip_pool_name == "osac-test-pool" + - _block_sc_result.resources[0].parameters.subsystem == "view-test-ensuresc-block-tier" + - "_block_sc_result.resources[0].parameters['csi.storage.k8s.io/provisioner-secret-name'] == 'vast-csi-test-ensuresc'" + - "_block_sc_result.resources[0].parameters['csi.storage.k8s.io/provisioner-secret-namespace'] == 'osac-system'" + - _block_sc_result.resources[0].parameters.storagePath is not defined + - _block_sc_result.resources[0].parameters.viewPolicy is not defined + - _block_sc_result.resources[0].parameters.secretName is not defined + fail_msg: "Block StorageClass parameters incorrect: {{ _block_sc_result.resources[0].parameters }}" + success_msg: "Block StorageClass parameters correct" + + - name: Assert block StorageClass labels + ansible.builtin.assert: + that: + - _block_sc_result.resources[0].metadata.labels['osac.openshift.io/storage-protocol'] == 'block' + - _block_sc_result.resources[0].metadata.labels['osac.openshift.io/storage-tier'] == 'block-tier' + fail_msg: "Block StorageClass labels incorrect" + success_msg: "Block StorageClass labels correct" + + # ── Verify CSI Secret ── + - name: Read CSI Secret + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: "vast-csi-test-ensuresc" + namespace: "osac-system" + register: _csi_secret + + - name: Assert CSI Secret exists + ansible.builtin.assert: + that: + - _csi_secret.resources | length == 1 + fail_msg: "CSI Secret 'vast-csi-test-ensuresc' was not created" + success_msg: "CSI Secret exists" + + - name: Assert CSI Secret uses tenant manager credentials (not admin creds) + ansible.builtin.assert: + that: + - _csi_secret.resources[0].data.username is defined + - (_csi_secret.resources[0].data.username | b64decode) == "osac-test-ensuresc" + - _csi_secret.resources[0].data.password is defined + - (_csi_secret.resources[0].data.password | b64decode) == "test-csi-password" + - _csi_secret.resources[0].data.endpoint is defined + - (_csi_secret.resources[0].data.endpoint | b64decode) == "127.0.0.1:18443" + - _csi_secret.resources[0].data.tenant is not defined + fail_msg: "CSI Secret does not contain expected manager credentials" + success_msg: "CSI Secret contains tenant manager credentials (tenant field omitted)" + + - name: Assert CSI Secret labels + ansible.builtin.assert: + that: + - _csi_secret.resources[0].metadata.labels['app.kubernetes.io/managed-by'] == 'osac-aap' + - _csi_secret.resources[0].metadata.labels['osac.openshift.io/tenant'] == 'test-ensuresc' + fail_msg: "CSI Secret labels incorrect" + success_msg: "CSI Secret labels correct" + + # ── Verify VolumeSnapshotClass ── + - name: Check if VolumeSnapshotClass CRD exists + kubernetes.core.k8s_info: + api_version: apiextensions.k8s.io/v1 + kind: CustomResourceDefinition + name: volumesnapshotclasses.snapshot.storage.k8s.io + register: _vsc_crd_check + + - name: Read VolumeSnapshotClass (when CRD exists) + when: _vsc_crd_check.resources | length > 0 + kubernetes.core.k8s_info: + api_version: snapshot.storage.k8s.io/v1 + kind: VolumeSnapshotClass + name: "vast-snapshot-test-ensuresc-default" + register: _vsc_result + + - name: Assert VolumeSnapshotClass exists (when CRD exists) + when: _vsc_crd_check.resources | length > 0 + ansible.builtin.assert: + that: + - _vsc_result.resources | length == 1 + fail_msg: "VolumeSnapshotClass 'vast-snapshot-test-ensuresc-default' was not created" + success_msg: "VolumeSnapshotClass exists" + + - name: Assert VolumeSnapshotClass properties (when CRD exists) + when: _vsc_crd_check.resources | length > 0 + ansible.builtin.assert: + that: + - _vsc_result.resources[0].driver == "csi.vastdata.com" + - _vsc_result.resources[0].deletionPolicy == "Delete" + - "_vsc_result.resources[0].parameters['csi.storage.k8s.io/snapshotter-secret-name'] == 'vast-csi-test-ensuresc'" + - "_vsc_result.resources[0].parameters['csi.storage.k8s.io/snapshotter-secret-namespace'] == 'osac-system'" + fail_msg: "VolumeSnapshotClass properties incorrect" + success_msg: "VolumeSnapshotClass properties correct" + + - name: Assert VolumeSnapshotClass labels (when CRD exists) + when: _vsc_crd_check.resources | length > 0 + ansible.builtin.assert: + that: + - _vsc_result.resources[0].metadata.labels['app.kubernetes.io/managed-by'] == 'osac-aap' + - _vsc_result.resources[0].metadata.labels['osac.openshift.io/tenant'] == 'test-ensuresc' + - _vsc_result.resources[0].metadata.labels['osac.openshift.io/storage-tier'] == 'default' + fail_msg: "VolumeSnapshotClass labels incorrect" + success_msg: "VolumeSnapshotClass labels correct" + + # ── Verify output facts ── + - name: Assert storage_provider_storage_class_names output was set + ansible.builtin.assert: + that: + - storage_provider_storage_class_names is defined + - storage_provider_storage_class_names is iterable + - "storage_provider_storage_class_names | map(attribute='name') | list is superset(['vast-nfs-test-ensuresc-default', 'vast-block-test-ensuresc-block-tier'])" + fail_msg: "storage_provider_storage_class_names not set correctly: {{ storage_provider_storage_class_names | default('undefined') }}" + success_msg: "storage_provider_storage_class_names output correct" + +# ── Test: Short-circuit on re-run ── +- name: "Storage Provider Ensure SC - Verify short-circuit" + hosts: localhost + gather_facts: false + + environment: + VAST_ENDPOINT: "127.0.0.1:18443" + VAST_USERNAME: "admin" + VAST_PASSWORD: "admin" + OSAC_STORAGE_CONFIG_NAMESPACE: "osac-system" + + vars: + tenant_name: "test-ensuresc" + tenant_namespace: "osac-system" + storage_provider_tiers: + - name: default + protocol: nfs + provider: vast + - name: block-tier + protocol: block + provider: vast + storage_provider_action: ensure_storage_class + storage_provider_provisioning_target: vmaas + storage_provider_snapshots_enabled: true + storage_provider_block_encryption_passphrase: "test-encryption-passphrase" + vast_storage_validate_certs: false + + tasks: + - name: Call ensure_storage_class again (should short-circuit) + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_action: ensure_storage_class + + - name: Assert output still set after short-circuit + ansible.builtin.assert: + that: + - storage_provider_storage_class_names is defined + - "storage_provider_storage_class_names | map(attribute='name') | list is superset(['vast-nfs-test-ensuresc-default', 'vast-block-test-ensuresc-block-tier'])" + fail_msg: "Short-circuit did not set output facts" + success_msg: "Short-circuit: output facts preserved" + +# ── Verify: VMS resources created during ensure_storage_class ── +- name: "Storage Provider Ensure SC - Verify VMS resource creation" + hosts: localhost + gather_facts: false + + tasks: + - name: Get mock server call log + ansible.builtin.uri: + url: "https://127.0.0.1:18443/_calls" + validate_certs: false + method: GET + return_content: true + register: _mock_calls + + - name: Parse mock call log + ansible.builtin.set_fact: + _call_log: "{{ _mock_calls.json }}" + + - name: Assert views were created + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'views') | selectattr('method', 'equalto', 'POST') | list | length > 0 + or _call_log | selectattr('path', 'search', 'views') | selectattr('method', 'equalto', 'GET') | list | length > 0 + fail_msg: "No view API calls found — ensure_storage_class should create views" + success_msg: "View API calls detected" + + - name: Assert view policies were created + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'viewpolicies') | list | length > 0 + fail_msg: "No view policy API calls found — ensure_storage_class should create view policies" + success_msg: "View policy API calls detected" + +# ── Cleanup ── +- name: "Storage Provider Ensure SC - Cleanup" + hosts: localhost + gather_facts: false + + tasks: + - name: Delete NFS StorageClass + kubernetes.core.k8s: + state: absent + api_version: storage.k8s.io/v1 + kind: StorageClass + name: "vast-nfs-test-ensuresc-default" + failed_when: false + + - name: Delete block StorageClass + kubernetes.core.k8s: + state: absent + api_version: storage.k8s.io/v1 + kind: StorageClass + name: "vast-block-test-ensuresc-block-tier" + failed_when: false + + - name: Delete CSI Secret + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "vast-csi-test-ensuresc" + namespace: "osac-system" + failed_when: false + + - name: Check if VolumeSnapshotClass CRD exists for cleanup + kubernetes.core.k8s_info: + api_version: apiextensions.k8s.io/v1 + kind: CustomResourceDefinition + name: volumesnapshotclasses.snapshot.storage.k8s.io + register: _cleanup_vsc_crd + + - name: Delete VolumeSnapshotClass + when: _cleanup_vsc_crd.resources | length > 0 + kubernetes.core.k8s: + state: absent + api_version: snapshot.storage.k8s.io/v1 + kind: VolumeSnapshotClass + name: "vast-snapshot-test-ensuresc-default" + failed_when: false + + - name: Delete hub Secret + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "vast-tenant-config-test-ensuresc" + namespace: "osac-system" + failed_when: false diff --git a/tests/integration/targets/storage_provider_onboarding/tasks/main.yml b/tests/integration/targets/storage_provider_onboarding/tasks/main.yml new file mode 100644 index 000000000..7e3b30632 --- /dev/null +++ b/tests/integration/targets/storage_provider_onboarding/tasks/main.yml @@ -0,0 +1,232 @@ +--- +# Integration test for chained setup → ensure_storage_class onboarding flow. +# Validates the credential handoff: ensure_storage_class reads the real hub +# Secret created by setup (no fixture injection). +# Requires: mock VMS server on 127.0.0.1:18443, kind cluster with osac-system namespace. + +# ── Setup: Reset state ── +- name: "Storage Onboarding - Reset state" + hosts: localhost + gather_facts: false + + tasks: + - name: Delete pre-existing test resources (clean slate) + block: + - name: Delete hub Secret if exists + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "vast-tenant-config-test-onboard" + namespace: "osac-system" + failed_when: false + + - name: Delete CSI Secret if exists + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "vast-csi-test-onboard" + namespace: "osac-system" + failed_when: false + + - name: Delete StorageClass if exists + kubernetes.core.k8s: + state: absent + api_version: storage.k8s.io/v1 + kind: StorageClass + name: "vast-nfs-test-onboard-default" + failed_when: false + + - name: Delete VolumeSnapshotClass if exists + kubernetes.core.k8s: + state: absent + api_version: snapshot.storage.k8s.io/v1 + kind: VolumeSnapshotClass + name: "vast-snapshot-test-onboard-default" + failed_when: false + +# ── Test: Chained setup → ensure_storage_class ── +- name: "Storage Onboarding - Run chained setup and ensure_storage_class" + hosts: localhost + gather_facts: false + + environment: + VAST_ENDPOINT: "127.0.0.1:18443" + VAST_USERNAME: "admin" + VAST_PASSWORD: "admin" + VAST_VIP_POOL_NAME: "osac-test-pool" + VAST_VIP_POOL_IP_RANGES: '[["10.0.0.10","10.0.0.50"]]' + VAST_VIP_POOL_SUBNET_CIDR: "24" + OSAC_STORAGE_CONFIG_NAMESPACE: "osac-system" + + vars: + tenant_name: "test-onboard" + tenant_namespace: "osac-system" + storage_provider_tiers: + - name: default + protocol: nfs + provider: vast + storage_provider_provisioning_target: vmaas + storage_provider_snapshots_enabled: false + vast_storage_validate_certs: false + + tasks: + - name: Run storage provider setup + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_action: setup + + - name: Run ensure_storage_class (uses real hub Secret from setup) + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_action: ensure_storage_class + +# ── Verify: Both hub Secret and StorageClass exist ── +- name: "Storage Onboarding - Verify results" + hosts: localhost + gather_facts: false + + vars: + tenant_name: "test-onboard" + + tasks: + - name: Read hub Secret + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: "vast-tenant-config-test-onboard" + namespace: "osac-system" + register: _hub_secret + + - name: Assert hub Secret exists + ansible.builtin.assert: + that: + - _hub_secret.resources | length == 1 + fail_msg: "Hub Secret 'vast-tenant-config-test-onboard' was not created by setup" + success_msg: "Hub Secret exists" + + - name: Decode hub Secret manager credentials + ansible.builtin.set_fact: + _hub_csi_username: "{{ _hub_secret.resources[0].data.tenant_manager_username | b64decode }}" + no_log: true + + - name: Read StorageClass + kubernetes.core.k8s_info: + api_version: storage.k8s.io/v1 + kind: StorageClass + name: "vast-nfs-test-onboard-default" + register: _sc_result + + - name: Assert StorageClass exists + ansible.builtin.assert: + that: + - _sc_result.resources | length == 1 + fail_msg: "StorageClass 'vast-nfs-test-onboard-default' was not created" + success_msg: "StorageClass exists" + + - name: Assert StorageClass parameters + ansible.builtin.assert: + that: + - _sc_result.resources[0].parameters.vip_pool_name == "osac-test-pool" + - _sc_result.resources[0].parameters.root_export == "/osac/test-onboard/default" + - "_sc_result.resources[0].parameters['csi.storage.k8s.io/provisioner-secret-name'] == 'vast-csi-test-onboard'" + - "_sc_result.resources[0].parameters['csi.storage.k8s.io/provisioner-secret-namespace'] == 'osac-system'" + fail_msg: "StorageClass parameters incorrect: {{ _sc_result.resources[0].parameters }}" + success_msg: "StorageClass parameters correct" + + - name: Read CSI Secret + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: "vast-csi-test-onboard" + namespace: "osac-system" + register: _csi_secret + + - name: Assert CSI Secret exists + ansible.builtin.assert: + that: + - _csi_secret.resources | length == 1 + fail_msg: "CSI Secret 'vast-csi-test-onboard' was not created" + success_msg: "CSI Secret exists" + + - name: Assert CSI Secret uses manager credentials from hub Secret (credential handoff) + ansible.builtin.assert: + that: + - _csi_secret.resources[0].data.username is defined + - (_csi_secret.resources[0].data.username | b64decode) == _hub_csi_username + - _csi_secret.resources[0].data.password is defined + - (_csi_secret.resources[0].data.password | b64decode | length) > 0 + - _csi_secret.resources[0].data.endpoint is defined + fail_msg: "CSI Secret credentials do not match hub Secret — credential handoff failed" + success_msg: "CSI Secret uses manager credentials from hub Secret (credential handoff validated)" + no_log: true + + - name: Assert CSI Secret does NOT contain admin credentials + ansible.builtin.assert: + that: + - (_csi_secret.resources[0].data.username | b64decode) != 'admin' + - (_csi_secret.resources[0].data.password | b64decode) != 'admin' + fail_msg: "CSI Secret contains admin credentials — security boundary violated" + success_msg: "CSI Secret confirmed free of admin credentials" + no_log: true + + - name: Assert StorageClass labels + ansible.builtin.assert: + that: + - _sc_result.resources[0].metadata.labels['app.kubernetes.io/managed-by'] == 'osac-aap' + - _sc_result.resources[0].metadata.labels['osac.openshift.io/tenant'] == 'test-onboard' + - _sc_result.resources[0].metadata.labels['osac.openshift.io/storage-tier'] == 'default' + - _sc_result.resources[0].metadata.labels['osac.openshift.io/storage-protocol'] == 'nfs' + fail_msg: "StorageClass labels incorrect" + success_msg: "StorageClass labels correct" + +# ── Cleanup ── +- name: "Storage Onboarding - Cleanup" + hosts: localhost + gather_facts: false + + tasks: + - name: Delete StorageClass + kubernetes.core.k8s: + state: absent + api_version: storage.k8s.io/v1 + kind: StorageClass + name: "vast-nfs-test-onboard-default" + failed_when: false + + - name: Delete CSI Secret + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "vast-csi-test-onboard" + namespace: "osac-system" + failed_when: false + + - name: Check if VolumeSnapshotClass CRD exists for cleanup + kubernetes.core.k8s_info: + api_version: apiextensions.k8s.io/v1 + kind: CustomResourceDefinition + name: volumesnapshotclasses.snapshot.storage.k8s.io + register: _cleanup_vsc_crd + + - name: Delete VolumeSnapshotClass + when: _cleanup_vsc_crd.resources | length > 0 + kubernetes.core.k8s: + state: absent + api_version: snapshot.storage.k8s.io/v1 + kind: VolumeSnapshotClass + name: "vast-snapshot-test-onboard-default" + failed_when: false + + - name: Delete hub Secret + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "vast-tenant-config-test-onboard" + namespace: "osac-system" + failed_when: false diff --git a/tests/integration/targets/storage_provider_setup/tasks/main.yml b/tests/integration/targets/storage_provider_setup/tasks/main.yml new file mode 100644 index 000000000..4a89d5480 --- /dev/null +++ b/tests/integration/targets/storage_provider_setup/tasks/main.yml @@ -0,0 +1,228 @@ +--- +# Integration test for osac.service.storage_provider setup action. +# Exercises the full tenant storage setup flow against a mock VMS server. +# Requires: mock VMS server on 127.0.0.1:18443, kind cluster with osac-system namespace. + +# ── Test: Full tenant storage setup ── +- name: "Storage Provider Setup - Run setup" + hosts: localhost + gather_facts: false + + environment: + VAST_ENDPOINT: "127.0.0.1:18443" + VAST_USERNAME: "admin" + VAST_PASSWORD: "admin" + VAST_VIP_POOL_NAME: "osac-test-pool" + VAST_VIP_POOL_IP_RANGES: '[["10.0.0.10","10.0.0.50"]]' + VAST_VIP_POOL_SUBNET_CIDR: "24" + OSAC_STORAGE_CONFIG_NAMESPACE: "osac-system" + + vars: + tenant_name: "test-setup" + tenant_namespace: "osac-system" + storage_provider_tiers: + - name: default + protocol: nfs + provider: vast + qos_policy: test-qos + qos_limits: + static_limits: + max_reads_bw_mbps: 100 + max_writes_bw_mbps: 100 + storage_provider_action: setup + storage_provider_provisioning_target: vmaas + storage_provider_snapshots_enabled: false + vast_storage_validate_certs: false + + tasks: + - name: Run storage provider setup + block: + - name: Delete any pre-existing hub Secret (clean slate) + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "vast-tenant-config-test-setup" + namespace: "osac-system" + + - name: Call storage_provider role with setup action + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_action: setup + + rescue: + - name: Capture setup failure details + ansible.builtin.set_fact: + _setup_test_failed: true + _setup_test_error: "{{ ansible_failed_result | default({}) }}" + + - name: Fail with context + ansible.builtin.fail: + msg: "Storage provider setup failed: {{ _setup_test_error }}" + +# ── Verify: Hub Secret created correctly ── +- name: "Storage Provider Setup - Verify results" + hosts: localhost + gather_facts: false + + environment: + OSAC_STORAGE_CONFIG_NAMESPACE: "osac-system" + + vars: + tenant_name: "test-setup" + + tasks: + - name: Read hub Secret + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: "vast-tenant-config-test-setup" + namespace: "osac-system" + register: _hub_secret + + - name: Assert hub Secret exists + ansible.builtin.assert: + that: + - _hub_secret.resources | length == 1 + fail_msg: "Hub Secret 'vast-tenant-config-test-setup' was not created" + success_msg: "Hub Secret exists" + + - name: Decode hub Secret fields + ansible.builtin.set_fact: + _secret_data: + tenant_manager_name: "{{ _hub_secret.resources[0].data.tenant_manager_name | b64decode }}" + tenant_manager_username: "{{ _hub_secret.resources[0].data.tenant_manager_username | b64decode }}" + tenant_manager_password: "{{ _hub_secret.resources[0].data.tenant_manager_password | b64decode }}" + tenant_manager_id: "{{ _hub_secret.resources[0].data.tenant_manager_id | b64decode }}" + tenant_role_id: "{{ _hub_secret.resources[0].data.tenant_role_id | b64decode }}" + vast_tenant_id: "{{ _hub_secret.resources[0].data.vast_tenant_id | b64decode }}" + vast_local_provider_id: "{{ _hub_secret.resources[0].data.vast_local_provider_id | b64decode }}" + vip_pool_name: "{{ _hub_secret.resources[0].data.vip_pool_name | b64decode }}" + vast_endpoint: "{{ _hub_secret.resources[0].data.vast_endpoint | b64decode }}" + storage_provider_type: "{{ _hub_secret.resources[0].data.storage_provider_type | b64decode }}" + + - name: Assert hub Secret has tenant manager name + ansible.builtin.assert: + that: + - _secret_data.tenant_manager_name == "osac-test-setup" + fail_msg: "Expected tenant_manager_name 'osac-test-setup', got '{{ _secret_data.tenant_manager_name }}'" + success_msg: "tenant_manager_name correct" + + - name: Assert hub Secret has manager credentials (non-empty) + ansible.builtin.assert: + that: + - _secret_data.tenant_manager_username | length > 0 + - _secret_data.tenant_manager_password | length > 0 + fail_msg: "tenant_manager_username or tenant_manager_password is empty" + success_msg: "tenant_manager credentials present" + + - name: Assert hub Secret has tenant ID (non-empty) + ansible.builtin.assert: + that: + - _secret_data.vast_tenant_id | string | length > 0 + - _secret_data.vast_tenant_id | string != "" + fail_msg: "vast_tenant_id is empty" + success_msg: "vast_tenant_id present: {{ _secret_data.vast_tenant_id }}" + + - name: Assert hub Secret has VIP pool name + ansible.builtin.assert: + that: + - _secret_data.vip_pool_name == "osac-test-pool" + fail_msg: "Expected vip_pool_name 'osac-test-pool', got '{{ _secret_data.vip_pool_name }}'" + success_msg: "vip_pool_name correct" + + - name: Assert hub Secret has endpoint + ansible.builtin.assert: + that: + - _secret_data.vast_endpoint == "127.0.0.1:18443" + fail_msg: "Expected vast_endpoint '127.0.0.1:18443', got '{{ _secret_data.vast_endpoint }}'" + success_msg: "vast_endpoint correct" + + - name: Assert hub Secret has storage provider type + ansible.builtin.assert: + that: + - _secret_data.storage_provider_type == "vast" + fail_msg: "Expected storage_provider_type 'vast', got '{{ _secret_data.storage_provider_type }}'" + success_msg: "storage_provider_type correct" + + - name: Assert hub Secret labels + ansible.builtin.assert: + that: + - _hub_secret.resources[0].metadata.labels['app.kubernetes.io/managed-by'] == 'osac-aap' + - _hub_secret.resources[0].metadata.labels['osac.openshift.io/tenant'] == 'test-setup' + fail_msg: "Hub Secret labels incorrect" + success_msg: "Hub Secret labels correct" + + - name: Assert storage_provider_tenant_configs output was set + ansible.builtin.assert: + that: + - storage_provider_tenant_configs is defined + - storage_provider_tenant_configs is mapping + - "'vast' in storage_provider_tenant_configs" + - storage_provider_tenant_configs.vast.vast_tenant_id is defined + - storage_provider_tenant_configs.vast.vip_pool_name == "osac-test-pool" + fail_msg: "storage_provider_tenant_configs output not set correctly: {{ storage_provider_tenant_configs | default('undefined') }}" + success_msg: "storage_provider_tenant_configs output correct" + + # ── Verify mock server received expected API calls ── + - name: Get mock server call log + ansible.builtin.uri: + url: "https://127.0.0.1:18443/_calls" + validate_certs: false + method: GET + return_content: true + register: _mock_calls + + - name: Parse mock call log + ansible.builtin.set_fact: + _call_log: "{{ _mock_calls.json }}" + + - name: Extract API methods and paths from call log + ansible.builtin.set_fact: + _call_summaries: >- + {{ _call_log | map(attribute='method') | zip(_call_log | map(attribute='path')) | map('join', ' ') | list }} + + - name: Assert VIP pool was created + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'vippools') | selectattr('method', 'equalto', 'POST') | list | length > 0 + or _call_log | selectattr('path', 'search', 'vippools') | selectattr('method', 'equalto', 'GET') | list | length > 0 + fail_msg: "No VIP pool API call found in mock call log" + success_msg: "VIP pool API call detected" + + - name: Assert tenant was created + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'tenants') | list | length > 0 + fail_msg: "No tenant creation API call found in mock call log" + success_msg: "Tenant creation API call detected" + + - name: Assert admin auth was performed + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'token') | selectattr('method', 'equalto', 'POST') | list | length > 0 + fail_msg: "No admin auth call found in mock call log" + success_msg: "Admin auth API call detected" + + - name: Assert manager was created + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'managers') | list | length > 0 + fail_msg: "No manager creation API call found in mock call log" + success_msg: "Manager creation API call detected" + +# ── Cleanup ── +- name: "Storage Provider Setup - Cleanup" + hosts: localhost + gather_facts: false + + tasks: + - name: Delete hub Secret + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "vast-tenant-config-test-setup" + namespace: "osac-system" + failed_when: false diff --git a/tests/integration/targets/storage_provider_setup_rollback/tasks/main.yml b/tests/integration/targets/storage_provider_setup_rollback/tasks/main.yml new file mode 100644 index 000000000..fc5401378 --- /dev/null +++ b/tests/integration/targets/storage_provider_setup_rollback/tasks/main.yml @@ -0,0 +1,141 @@ +--- +# Integration test for setup rescue/rollback path. +# Injects a failure on view creation so setup hits the rescue block, +# which calls delete_qos_policy.yaml with _provider_tiers (not _vast_teardown_tiers). +# Verifies the rollback completes without undefined variable errors. +# Requires: mock VMS server on 127.0.0.1:18443, kind cluster with osac-system namespace. + +# ── Setup: Clean slate + inject failure ── +- name: "Storage Provider Setup Rollback - Prepare" + hosts: localhost + gather_facts: false + + tasks: + - name: Delete any pre-existing hub Secret + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "vast-tenant-config-test-rollback" + namespace: "osac-system" + failed_when: false + + - name: Inject failure on manager creation (triggers rescue block) + ansible.builtin.uri: + url: "https://127.0.0.1:18443/_inject_failure" + validate_certs: false + method: POST + body_format: json + body: + resource: managers + method: POST + status: 500 + body: + error: "injected: simulating manager creation failure" + +# ── Test: Setup should fail but rescue should complete cleanly ── +- name: "Storage Provider Setup Rollback - Run setup (expect failure)" + hosts: localhost + gather_facts: false + + environment: + VAST_ENDPOINT: "127.0.0.1:18443" + VAST_USERNAME: "admin" + VAST_PASSWORD: "admin" + VAST_VIP_POOL_NAME: "osac-test-pool" + VAST_VIP_POOL_IP_RANGES: '[["10.0.0.10","10.0.0.50"]]' + VAST_VIP_POOL_SUBNET_CIDR: "24" + OSAC_STORAGE_CONFIG_NAMESPACE: "osac-system" + + vars: + tenant_name: "test-rollback" + tenant_namespace: "osac-system" + storage_provider_tiers: + - name: default + protocol: nfs + provider: vast + qos_policy: rollback-qos + qos_limits: + static_limits: + max_reads_bw_mbps: 100 + storage_provider_action: setup + storage_provider_provisioning_target: vmaas + storage_provider_snapshots_enabled: false + vast_storage_validate_certs: false + + tasks: + - name: Run setup and capture failure + block: + - name: Call storage_provider role with setup action (should fail) + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_action: setup + + - name: Fail test if setup unexpectedly succeeded + ansible.builtin.fail: + msg: "Setup should have failed due to injected view creation error but succeeded" + + rescue: + - name: Assert setup failed as expected (rescue block executed) + ansible.builtin.debug: + msg: "Setup failed as expected — rescue block executed: {{ ansible_failed_result.msg | default('') }}" + +# ── Verify: Rollback cleaned up and no undefined variable crash ── +- name: "Storage Provider Setup Rollback - Verify cleanup" + hosts: localhost + gather_facts: false + + tasks: + - name: Get mock server call log + ansible.builtin.uri: + url: "https://127.0.0.1:18443/_calls" + validate_certs: false + method: GET + return_content: true + register: _mock_calls + + - name: Assert rollback queried VMS resources for cleanup + ansible.builtin.assert: + that: + - _mock_calls.json | selectattr('path', 'search', 'views.*tenant_id=') | selectattr('method', 'equalto', 'GET') | list | length > 0 + or _mock_calls.json | selectattr('path', 'search', 'qospolicies.*tenant_id=') | selectattr('method', 'equalto', 'GET') | list | length > 0 + fail_msg: "Rollback did not query VMS resources by tenant_id for cleanup" + success_msg: "Rollback queried VMS resources by tenant_id" + + - name: Assert rollback attempted tenant deletion + ansible.builtin.assert: + that: + - _mock_calls.json | selectattr('path', 'search', 'tenants') | selectattr('method', 'equalto', 'DELETE') | list | length > 0 + fail_msg: "Rollback did not attempt tenant deletion" + success_msg: "Rollback attempted tenant deletion" + + - name: Verify hub Secret was NOT persisted (rollback should prevent it) + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: "vast-tenant-config-test-rollback" + namespace: "osac-system" + register: _hub_secret_check + + - name: Assert hub Secret does not exist after rollback + ansible.builtin.assert: + that: + - _hub_secret_check.resources | length == 0 + fail_msg: "Hub Secret should not exist after setup rollback" + success_msg: "Hub Secret correctly absent after rollback" + +# ── Cleanup ── +- name: "Storage Provider Setup Rollback - Cleanup" + hosts: localhost + gather_facts: false + + tasks: + - name: Delete hub Secret if somehow created + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "vast-tenant-config-test-rollback" + namespace: "osac-system" + failed_when: false diff --git a/tests/integration/targets/storage_provider_teardown/tasks/main.yml b/tests/integration/targets/storage_provider_teardown/tasks/main.yml new file mode 100644 index 000000000..1f39e564c --- /dev/null +++ b/tests/integration/targets/storage_provider_teardown/tasks/main.yml @@ -0,0 +1,465 @@ +--- +# Integration test for osac.service.storage_provider teardown action. +# Tests the full teardown lifecycle: deletes StorageClasses (by label, including +# JIT-created ones), CSI Secret, hub Secret, and verifies mock VMS server +# received live-query delete calls. +# Requires: mock VMS server on 127.0.0.1:18443, kind cluster with osac-system namespace. + +# ── Setup: Pre-create resources to be torn down ── +- name: "Storage Provider Teardown - Setup fixtures" + hosts: localhost + gather_facts: false + + tasks: + - name: Reset mock VMS server (sequential execution — safe to reset) + ansible.builtin.uri: + url: "https://127.0.0.1:18443/_reset" + validate_certs: false + method: POST + failed_when: false + + - name: Pre-seed mock VMS with tenant resources for live-query teardown + ansible.builtin.uri: + url: "https://127.0.0.1:18443/api/{{ item.resource }}/" + method: POST + body_format: json + body: "{{ item.body }}" + validate_certs: false + status_code: [200, 201] + loop: + - resource: tenants + body: + name: "test-teardown" + local_provider_id: 1 + - resource: views + body: + name: "view-test-teardown-default" + path: "/osac/test-teardown/default" + tenant_id: 1 + - resource: views + body: + name: "view-test-teardown-extra" + path: "/osac/test-teardown/extra" + tenant_id: 1 + - resource: quotas + body: + name: "quota-test-teardown-default" + path: "/osac/test-teardown/default" + tenant_id: 1 + - resource: viewpolicies + body: + name: "osac-test-teardown-nfs" + flavor: "NFS" + tenant_id: 1 + - resource: qospolicies + body: + name: "osac-test-teardown-default" + tenant_id: 1 + mode: "STATIC" + static_limits: + max_reads_bw_mbps: 100 + - resource: managers + body: + username: "osac-test-teardown" + roles: [1] + - resource: roles + body: + name: "osac-csi-test-teardown" + loop_control: + label: "{{ item.resource }}/{{ item.body.name | default(item.body.username | default('?')) }}" + + - name: Create hub Secret fixture + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Secret + metadata: + name: "vast-tenant-config-test-teardown" + namespace: "osac-system" + labels: + app.kubernetes.io/managed-by: osac-aap + osac.openshift.io/tenant: "test-teardown" + type: Opaque + stringData: + vast_tenant_id: "1" + vast_local_provider_id: "1" + vip_pool_name: "osac-test-pool" + vast_endpoint: "127.0.0.1:18443" + storage_provider_type: "vast" + tenant_manager_name: "osac-test-teardown" + tenant_manager_username: "osac-test-teardown" + tenant_manager_password: "test-csi-password" + tenant_manager_id: "7" + tenant_role_id: "8" + + - name: Create StorageClass fixture (setup-created) + kubernetes.core.k8s: + state: present + definition: + apiVersion: storage.k8s.io/v1 + kind: StorageClass + metadata: + name: "vast-nfs-test-teardown-default" + labels: + app.kubernetes.io/managed-by: osac-aap + osac.openshift.io/tenant: "test-teardown" + osac.openshift.io/storage-tier: "default" + osac.openshift.io/storage-protocol: "nfs" + provisioner: csi.vastdata.com + parameters: + vip_pool_name: "osac-test-pool" + root_export: "/osac/test-teardown/default" + csi.storage.k8s.io/provisioner-secret-name: "vast-csi-test-teardown" + csi.storage.k8s.io/provisioner-secret-namespace: "osac-system" + reclaimPolicy: Delete + volumeBindingMode: Immediate + + - name: Create JIT StorageClass fixture (ensure_storage_class-created, extra tier) + kubernetes.core.k8s: + state: present + definition: + apiVersion: storage.k8s.io/v1 + kind: StorageClass + metadata: + name: "vast-block-test-teardown-performance" + labels: + app.kubernetes.io/managed-by: osac-aap + osac.openshift.io/tenant: "test-teardown" + osac.openshift.io/storage-tier: "performance" + osac.openshift.io/storage-protocol: "block" + provisioner: block.csi.vastdata.com + parameters: + vip_pool_name: "osac-test-pool" + subsystem: "view-test-teardown-performance" + tenant_name: "test-teardown" + reclaimPolicy: Delete + volumeBindingMode: WaitForFirstConsumer + + - name: Create CSI Secret fixture + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Secret + metadata: + name: "vast-csi-test-teardown" + namespace: "osac-system" + labels: + app.kubernetes.io/managed-by: osac-aap + osac.openshift.io/tenant: "test-teardown" + type: Opaque + stringData: + token: "test-api-token-for-csi" + endpoint: "127.0.0.1:18443" + + - name: Check if VolumeSnapshotClass CRD exists + kubernetes.core.k8s_info: + api_version: apiextensions.k8s.io/v1 + kind: CustomResourceDefinition + name: volumesnapshotclasses.snapshot.storage.k8s.io + register: _vsc_crd_check + + - name: Create VolumeSnapshotClass fixture (when CRD exists) + when: _vsc_crd_check.resources | length > 0 + kubernetes.core.k8s: + state: present + definition: + apiVersion: snapshot.storage.k8s.io/v1 + kind: VolumeSnapshotClass + metadata: + name: "vast-snapshot-test-teardown-default" + labels: + app.kubernetes.io/managed-by: osac-aap + osac.openshift.io/tenant: "test-teardown" + osac.openshift.io/storage-tier: "default" + driver: csi.vastdata.com + deletionPolicy: Delete + parameters: + csi.storage.k8s.io/snapshotter-secret-name: "vast-csi-test-teardown" + csi.storage.k8s.io/snapshotter-secret-namespace: "osac-system" + + - name: Verify all fixtures created + block: + - name: Verify hub Secret exists + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: "vast-tenant-config-test-teardown" + namespace: "osac-system" + register: _verify_hub + + - name: Verify both StorageClasses exist (setup + JIT) + kubernetes.core.k8s_info: + api_version: storage.k8s.io/v1 + kind: StorageClass + label_selectors: + - "osac.openshift.io/tenant=test-teardown" + register: _verify_scs + + - name: Assert all fixtures are in place + ansible.builtin.assert: + that: + - _verify_hub.resources | length == 1 + - _verify_scs.resources | length == 2 + fail_msg: "Not all fixtures were created successfully (expected 1 hub Secret, 2 SCs)" + success_msg: "All fixtures created: hub Secret, 2 StorageClasses (setup + JIT), CSI Secret" + +# ── Test: Run teardown ── +- name: "Storage Provider Teardown - Run teardown" + hosts: localhost + gather_facts: false + + environment: + VAST_ENDPOINT: "127.0.0.1:18443" + VAST_USERNAME: "admin" + VAST_PASSWORD: "admin" + OSAC_STORAGE_CONFIG_NAMESPACE: "osac-system" + + vars: + tenant_name: "test-teardown" + tenant_namespace: "osac-system" + storage_provider_tiers: + - name: default + protocol: nfs + provider: vast + storage_provider_provisioning_target: vmaas + storage_provider_action: teardown + vast_storage_validate_certs: false + + tasks: + - name: Call storage_provider role with teardown action + ansible.builtin.include_role: + name: osac.service.storage_provider + vars: + storage_provider_action: teardown + +# ── Verify: K8s resources deleted (including JIT SCs) ── +- name: "Storage Provider Teardown - Verify K8s resource deletion" + hosts: localhost + gather_facts: false + + tasks: + - name: Check all tenant StorageClasses deleted (by label) + kubernetes.core.k8s_info: + api_version: storage.k8s.io/v1 + kind: StorageClass + label_selectors: + - "osac.openshift.io/tenant=test-teardown" + register: _td_scs + + - name: Assert ALL tenant StorageClasses deleted (including JIT) + ansible.builtin.assert: + that: + - _td_scs.resources | length == 0 + fail_msg: >- + {{ _td_scs.resources | length }} StorageClass(es) still exist: + {{ _td_scs.resources | map(attribute='metadata.name') | list }} + success_msg: "All tenant StorageClasses deleted (setup + JIT)" + + - name: Check CSI Secret deleted + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: "vast-csi-test-teardown" + namespace: "osac-system" + register: _td_csi + + - name: Assert CSI Secret deleted + ansible.builtin.assert: + that: + - _td_csi.resources | length == 0 + fail_msg: "CSI Secret 'vast-csi-test-teardown' was not deleted" + success_msg: "CSI Secret deleted" + + - name: Check hub Secret deleted (should be deleted when cleanup succeeds) + kubernetes.core.k8s_info: + api_version: v1 + kind: Secret + name: "vast-tenant-config-test-teardown" + namespace: "osac-system" + register: _td_hub + + - name: Assert hub Secret deleted + ansible.builtin.assert: + that: + - _td_hub.resources | length == 0 + fail_msg: "Hub Secret 'vast-tenant-config-test-teardown' was not deleted (cleanup may have failed)" + success_msg: "Hub Secret deleted" + + - name: Check VolumeSnapshotClass CRD exists for verification + kubernetes.core.k8s_info: + api_version: apiextensions.k8s.io/v1 + kind: CustomResourceDefinition + name: volumesnapshotclasses.snapshot.storage.k8s.io + register: _td_vsc_crd + + - name: Check VolumeSnapshotClass deleted (when CRD exists) + when: _td_vsc_crd.resources | length > 0 + kubernetes.core.k8s_info: + api_version: snapshot.storage.k8s.io/v1 + kind: VolumeSnapshotClass + label_selectors: + - "osac.openshift.io/tenant=test-teardown" + register: _td_vsc + + - name: Assert VolumeSnapshotClass deleted (when CRD exists) + when: _td_vsc_crd.resources | length > 0 + ansible.builtin.assert: + that: + - _td_vsc.resources | length == 0 + fail_msg: "VolumeSnapshotClass(es) still exist for tenant test-teardown" + success_msg: "All VolumeSnapshotClasses deleted" + +# ── Verify: Mock server received live-query delete calls ── +- name: "Storage Provider Teardown - Verify mock API calls" + hosts: localhost + gather_facts: false + + tasks: + - name: Get mock server call log + ansible.builtin.uri: + url: "https://127.0.0.1:18443/_calls" + validate_certs: false + method: GET + return_content: true + register: _mock_calls + + - name: Parse mock call log + ansible.builtin.set_fact: + _call_log: "{{ _mock_calls.json }}" + + - name: Assert admin auth was performed for VMS cleanup + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'equalto', '/api/token/') | selectattr('method', 'equalto', 'POST') | list | length > 0 + fail_msg: "No admin auth call found — teardown did not authenticate to VMS" + success_msg: "Admin auth call detected" + + - name: Assert views were queried by tenant_id (live query) + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'views.*tenant_id=') | selectattr('method', 'equalto', 'GET') | list | length > 0 + fail_msg: "No live view query by tenant_id found in mock call log" + success_msg: "Views queried by tenant_id" + + - name: Assert view DELETE calls were made + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'views/') | selectattr('method', 'equalto', 'DELETE') | list | length > 0 + fail_msg: "No view DELETE calls found in mock call log" + success_msg: "View DELETE calls detected" + + - name: Assert quotas were queried by tenant_id (live query) + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'quotas.*tenant_id=') | selectattr('method', 'equalto', 'GET') | list | length > 0 + fail_msg: "No live quota query by tenant_id found in mock call log" + success_msg: "Quotas queried by tenant_id" + + - name: Assert view policies were queried by tenant_id (live query) + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'viewpolicies.*tenant_id=') | selectattr('method', 'equalto', 'GET') | list | length > 0 + fail_msg: "No live view policy query by tenant_id found in mock call log" + success_msg: "View policies queried by tenant_id" + + - name: Assert QoS policies were queried by tenant_id (live query) + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'qospolicies.*tenant_id=') | selectattr('method', 'equalto', 'GET') | list | length > 0 + fail_msg: "No live QoS policy query by tenant_id found in mock call log" + success_msg: "QoS policies queried by tenant_id" + + - name: Assert tenant deletion was attempted + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'tenants') | list | length > 0 + fail_msg: "No tenant API call found in mock call log" + success_msg: "Tenant API call detected" + + - name: Assert manager lookup was attempted + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'managers') | list | length > 0 + fail_msg: "No manager API call found in mock call log" + success_msg: "Manager API call detected" + + - name: Assert role lookup was attempted + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'roles') | list | length > 0 + fail_msg: "No role API call found in mock call log" + success_msg: "Role API call detected" + + - name: Assert local provider cleanup was attempted + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'localproviders') | selectattr('method', 'equalto', 'GET') | list | length > 0 + fail_msg: "No local provider lookup found in mock call log — orphaned providers would persist" + success_msg: "Local provider cleanup attempted" + + - name: Assert local provider was deleted + ansible.builtin.assert: + that: + - _call_log | selectattr('path', 'search', 'localproviders/') | selectattr('method', 'equalto', 'DELETE') | list | length > 0 + fail_msg: "No local provider DELETE call found — orphaned provider not cleaned up" + success_msg: "Local provider deleted" + + - name: Verify mock VMS has no remaining local providers for this tenant + ansible.builtin.uri: + url: "https://127.0.0.1:18443/api/localproviders/?name=provider-test-teardown" + validate_certs: false + method: GET + return_content: true + register: _td_lp_check + + - name: Assert no orphaned local providers remain + ansible.builtin.assert: + that: + - _td_lp_check.json | length == 0 + fail_msg: "Orphaned local provider 'provider-test-teardown' still exists in VMS" + success_msg: "No orphaned local providers" + +# ── Cleanup: Best-effort removal of any remaining resources ── +- name: "Storage Provider Teardown - Final cleanup" + hosts: localhost + gather_facts: false + + tasks: + - name: Delete hub Secret (safety net) + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "vast-tenant-config-test-teardown" + namespace: "osac-system" + failed_when: false + + - name: Delete any remaining tenant StorageClasses (safety net) + kubernetes.core.k8s_info: + api_version: storage.k8s.io/v1 + kind: StorageClass + label_selectors: + - "osac.openshift.io/tenant=test-teardown" + register: _cleanup_scs + + - name: Remove remaining StorageClasses + kubernetes.core.k8s: + state: absent + api_version: storage.k8s.io/v1 + kind: StorageClass + name: "{{ item.metadata.name }}" + loop: "{{ _cleanup_scs.resources | default([]) }}" + loop_control: + label: "{{ item.metadata.name }}" + failed_when: false + + - name: Delete CSI Secret (safety net) + kubernetes.core.k8s: + state: absent + api_version: v1 + kind: Secret + name: "vast-csi-test-teardown" + namespace: "osac-system" + failed_when: false diff --git a/tests/integration/teardown_test_env.sh b/tests/integration/teardown_test_env.sh index 5cfacc7c5..390f10a69 100755 --- a/tests/integration/teardown_test_env.sh +++ b/tests/integration/teardown_test_env.sh @@ -3,11 +3,23 @@ set -e echo "=== Tearing down test environment ===" +# Stop mock VMS server if running +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "${SCRIPT_DIR}/.mock_vms_pid" ]; then + MOCK_VMS_PID=$(cat "${SCRIPT_DIR}/.mock_vms_pid") + echo "Stopping mock VMS server (PID: ${MOCK_VMS_PID})..." + kill "${MOCK_VMS_PID}" 2>/dev/null || true + rm -f "${SCRIPT_DIR}/.mock_vms_pid" +fi + # Delete kind cluster -KIND_EXPERIMENTAL_PROVIDER=podman kind delete cluster --name osac-test +kind delete cluster --name osac-test # Clean up temporary files rm -f /tmp/osac_test_overrides.log rm -rf /tmp/osac-operator +rm -rf "${SCRIPT_DIR}/certs" +rm -f "${SCRIPT_DIR}/kubeconfig-osac-test" +rm -f "${SCRIPT_DIR}/.storage_env" echo "=== Cleanup complete ===" diff --git a/vendor/ansible_collections/vastdata.vms-1.2.0.info/GALAXY.yml b/vendor/ansible_collections/vastdata.vms-1.2.0.info/GALAXY.yml new file mode 100644 index 000000000..b0480b27f --- /dev/null +++ b/vendor/ansible_collections/vastdata.vms-1.2.0.info/GALAXY.yml @@ -0,0 +1,8 @@ +download_url: https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/artifacts/vastdata-vms-1.2.0.tar.gz +format_version: 1.0.0 +name: vms +namespace: vastdata +server: https://galaxy.ansible.com/api/ +signatures: [] +version: 1.2.0 +version_url: /api/v3/plugin/ansible/content/published/collections/index/vastdata/vms/versions/1.2.0/ diff --git a/vendor/ansible_collections/vastdata/vms/CHANGELOG.md b/vendor/ansible_collections/vastdata/vms/CHANGELOG.md new file mode 100644 index 000000000..8a5c954a8 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/CHANGELOG.md @@ -0,0 +1,58 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.2.0] - 2026-05-05 + +### Added + +- New modules: + - `vastdata.vms.protectionpolicies` + - `vastdata.vms.protectedpaths` + - `vastdata.vms.globalsnapstreams` + - `vastdata.vms.snapshots` + - `vastdata.vms.nativereplicationremotetargets` + - `vastdata.vms.user_key` + + +## [1.1.0] - 2026-03-29 + +### Added + +- New modules for managing non-local identity providers: + - `vastdata.vms.nonlocal_group` - Manage non-local groups + - `vastdata.vms.nonlocal_user` - Manage non-local users +- New module `vastdata.vms.eventdefinitionconfigs` - Manage event definition configurations +- Debug tracing and centralized timeout support for improved troubleshooting +- Galaxy version and git commit stamped into User-Agent header for request traceability + +### Changed + +- Replaced `vastpy` SDK dependency with a self-contained REST client (`VastClient`) + + +## [1.0.0] - 2025-02-26 + +### Added - First Public Release + +- Initial public release of VAST Ansible Collection +- 10 core modules for VAST storage management: + - `vastdata.vms.views` - Manage VAST views (file system exports) + - `vastdata.vms.viewpolicies` - Manage view policies and configurations + - `vastdata.vms.vippools` - Manage VIP pools for network configuration + - `vastdata.vms.quotas` - Manage storage quotas + - `vastdata.vms.s3policies` - Manage S3 bucket policies + - `vastdata.vms.tenants` - Manage multi-tenancy configurations + - `vastdata.vms.groups` - Manage user groups + - `vastdata.vms.users` - Manage user accounts + - `vastdata.vms.ldaps` - Configure LDAP authentication + - `vastdata.vms.dns` - Manage DNS settings +- Authentication via token (VAST 5.3+) or username/password +- Full idempotency and check mode support +- Diff mode for change preview +- Comprehensive module documentation +- Unit and sanity test coverage +- Python 3.9+ and ansible-core 2.14+ support diff --git a/vendor/ansible_collections/vastdata/vms/FILES.json b/vendor/ansible_collections/vastdata/vms/FILES.json new file mode 100644 index 000000000..37a2f3ab8 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/FILES.json @@ -0,0 +1,1335 @@ +{ + "files": [ + { + "name": ".", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "README.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "df6ca2507dff673bf1166f514c9a176571d894be91ffbabf48d30976383c6be5", + "format": 1 + }, + { + "name": "docs", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "docs/.gitkeep", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "meta/runtime.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e37a1b13055df51be43bb9024837ba37bcd6504d14bfa874f0f7171714104402", + "format": 1 + }, + { + "name": "plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/vast", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/module_utils/vast/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b86743f845f64924b5ba03e12bec6f07b9f2027a652fbbaaad6ce7b07565f569", + "format": 1 + }, + { + "name": "plugins/module_utils/vast/auth.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "85ac4af3d4776a347c53e4b1aa47e54afe04643c3d6763fbb338998cc68a334a", + "format": 1 + }, + { + "name": "plugins/module_utils/vast/client.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "66235eae31fe3ee5660299c7455fced6895e9874d26547f526e3b6e7026f1bbd", + "format": 1 + }, + { + "name": "plugins/module_utils/vast/diff.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "530673884bfbb72caf7ed3111668b1805d5010c9703860014829cf06755f9c83", + "format": 1 + }, + { + "name": "plugins/module_utils/vast/errors.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1d30e4b44a54649118c8cae1dcc9af6a831b5e0decdac3bfc3eafaff10551828", + "format": 1 + }, + { + "name": "plugins/module_utils/vast/module_customizations.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b702391487b190b43693afe2434d11b25aaa8cef465593c486acd0e5fa4eb896", + "format": 1 + }, + { + "name": "plugins/module_utils/vast/protectionpolicies_utils.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d2f756761960209d75dcb84bf72ed40c83d1e9c0ab41d04d5cc2fbf01f24a180", + "format": 1 + }, + { + "name": "plugins/module_utils/vast/resource.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "03575dd878f634856db12f5788c572c39b5f3aacad6a5ec3328cb1733962a239", + "format": 1 + }, + { + "name": "plugins/module_utils/vast/s3_attachment_utils.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "87127b71a7b394ecdb54f229c8ef6288f1c5ab1ced15cd16e61092644608b3fd", + "format": 1 + }, + { + "name": "plugins/module_utils/vast/schema_overrides.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6ec07cedf691cd64ddecb5e9684c1242de546f24680522d3cc76bf6286309ae5", + "format": 1 + }, + { + "name": "plugins/module_utils/vast/sub_endpoint_resource.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c141e2ef0322665090f29d64a08ce5b013cd15ccf467506227db4b5dc409e318", + "format": 1 + }, + { + "name": "plugins/module_utils/vast/timeouts.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5a5c3511539c072989d23166dc6b363383771057b704534acba66503cc0149e3", + "format": 1 + }, + { + "name": "plugins/module_utils/vast/version.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "33b9630db4e5c67a7c8ce16adce02419409107d4f55fa47742b06d01fc944ce7", + "format": 1 + }, + { + "name": "plugins/module_utils/vast/_build_info.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7287328c288acc956977a787fdf8966afdb07448258888c319b6b525918e6966", + "format": 1 + }, + { + "name": "plugins/modules", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/modules/dns.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a17913034bdc3acf9045cc45030981fec1e76fba751d3822eb7058cd60629e1e", + "format": 1 + }, + { + "name": "plugins/modules/eventdefinitionconfigs.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a25f1861d1827f1c977cb667ce8beb8c04b6c0dc5cfe7ba00b87df7e6e57417d", + "format": 1 + }, + { + "name": "plugins/modules/globalsnapstreams.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "dbe48a8b6ba61fb46dbbec9a4410219500a339efc114a3e6bc5906d43b2bb7dd", + "format": 1 + }, + { + "name": "plugins/modules/groups.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c5d555b2dc393ae725e88be37737b3f72092d22f7780a0e12cf16b7d66702e5f", + "format": 1 + }, + { + "name": "plugins/modules/ldaps.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fe6e3b865d531416df259aedd9a26eaec8b462dff6c8d6773402126373e34ba1", + "format": 1 + }, + { + "name": "plugins/modules/nativereplicationremotetargets.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0a9ac227d21e89350a6d101c26bed39a96d828dac8158a7dcdce57f0b5622f3c", + "format": 1 + }, + { + "name": "plugins/modules/nonlocal_group.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "fe985bf123435dfc9fd308d3911b7d2301f048bdadf222ff41bb18361cea4ec6", + "format": 1 + }, + { + "name": "plugins/modules/nonlocal_user.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5288847c475dbca3ec50d180e513386baff1bb0d88ec3639abccc09680821cd4", + "format": 1 + }, + { + "name": "plugins/modules/protectedpaths.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ffaef04b55679c708050a43677a68a7a1dbd312888eaf93521602cc142313831", + "format": 1 + }, + { + "name": "plugins/modules/protectionpolicies.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d45281a2d315eb8a3e82b1a7baca0bd0d1ec888ee84ee3e99f1d08a082c7f41c", + "format": 1 + }, + { + "name": "plugins/modules/quotas.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "04ed073f50f7638e739266bbc5d7f0cd8f9b9d34ce3f4912336f0e64658ba6a3", + "format": 1 + }, + { + "name": "plugins/modules/s3policies.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ce16f626bc9dcbe42604b92dc060f91314d705fb654f394f37e7625ed11de5db", + "format": 1 + }, + { + "name": "plugins/modules/snapshots.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1168372ce42f88a125c415c6389130ad59634c3b169c405fe3c004b7e9068ac4", + "format": 1 + }, + { + "name": "plugins/modules/tenants.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "af9434d2a2d7c7fe5c121796881f84479fb2732537544a8d32887115f785fa43", + "format": 1 + }, + { + "name": "plugins/modules/user_key.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "618770fe6ad3d8920337ac71c2349001628b372e6570f289ae18e51b87e0291c", + "format": 1 + }, + { + "name": "plugins/modules/users.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "050f25392e91d2cd3d108153a834c8934b5b128a96c7267479173a58d8138090", + "format": 1 + }, + { + "name": "plugins/modules/viewpolicies.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "aa049fe6c524d8e6cf91c7ba9f2cc0501bc3068a3ce1d3c9ad0dfe014fb275fa", + "format": 1 + }, + { + "name": "plugins/modules/views.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ea1168f0aeb38f4501ce5c2736aa631c926be0b31d5a9844f48fee8a18128391", + "format": 1 + }, + { + "name": "plugins/modules/vippools.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "39aecf5b63a87c576c974c851bba92c3c3f944441177896d5e6acca20540fb29", + "format": 1 + }, + { + "name": "tests", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/.gitignore", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "6aca2e456d19a6afaad4234f8d575441d83934652f850b04771fa36eb733a720", + "format": 1 + }, + { + "name": "tests/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/integration", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/requirements.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b51a569e89e84f7065d347436a460236cd4ebfc166d2e7124ff3299a69f8ef58", + "format": 1 + }, + { + "name": "tests/integration/targets", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/.gitkeep", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/integration/targets/cnodes", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/cnodes/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/cnodes/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b3efd3a33c15177bc8a8c28354c0c37482db9e4f563a8748bbdd3b3004b28dd4", + "format": 1 + }, + { + "name": "tests/integration/targets/cnodes/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/cnodes/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "270f6d06273e63220108061324131a1792f0e61aad940c5e1882d6c38ff4a7cb", + "format": 1 + }, + { + "name": "tests/integration/targets/dns", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/dns/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/dns/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b4ef67f7a69ddfee8ed95b13e93d3c0e0927060e09c9fd63718012dcccf31b8c", + "format": 1 + }, + { + "name": "tests/integration/targets/dns/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/dns/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "df84728173b19daee1d9090e0c2d338d5b1ae9a63aa91d7a4e3da78fa3de01c5", + "format": 1 + }, + { + "name": "tests/integration/targets/eventdefinitionconfigs", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/eventdefinitionconfigs/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/eventdefinitionconfigs/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2b4b224604f5e4be399e0b7624d86fe00188d308ff0eefada8924ac1b6bc6db9", + "format": 1 + }, + { + "name": "tests/integration/targets/eventdefinitionconfigs/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/eventdefinitionconfigs/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2e6538113121388251579f0cfd4e3b4d46bcd666ce0a14e9146eee9ed0bd6a11", + "format": 1 + }, + { + "name": "tests/integration/targets/globalsnapstreams", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/globalsnapstreams/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/globalsnapstreams/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9f4f2d16811f69c8ce7bd0b9292250c5c120233c1c1bd59dc0643aee00dea0c2", + "format": 1 + }, + { + "name": "tests/integration/targets/globalsnapstreams/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/globalsnapstreams/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7188feeb69f938f08986291f09305c85c8d7aec8fba6c39ce52802571ea948d2", + "format": 1 + }, + { + "name": "tests/integration/targets/groups", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/groups/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/groups/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "63d256d139eb5f222ba5bed7edd692657b8d2ed485ab320f2f801c2c64d9fee0", + "format": 1 + }, + { + "name": "tests/integration/targets/groups/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/groups/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e17ae549d96343df63e46901d3e5e6150a7ff57324bf9e5c91932246a2bdb0e1", + "format": 1 + }, + { + "name": "tests/integration/targets/ldaps", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/ldaps/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/ldaps/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5c3e81d3f948411754f3aea9f024b33f0c016cf501fb95d3b4efdcf260dd3927", + "format": 1 + }, + { + "name": "tests/integration/targets/ldaps/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/ldaps/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "78241c325b2fa2220c780531df0cc88d0599b3f07d466744a36cb6f05996a328", + "format": 1 + }, + { + "name": "tests/integration/targets/nativereplicationremotetargets", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/nativereplicationremotetargets/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/nativereplicationremotetargets/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d6e9e43af96f34443c04c9ce433484fae63138042b3090982d5de4cba6ac9d2f", + "format": 1 + }, + { + "name": "tests/integration/targets/nativereplicationremotetargets/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/nativereplicationremotetargets/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "875a0fc62ef29f5516f4e4a0a06e81656a8616534cae5964d3b684f93c38822c", + "format": 1 + }, + { + "name": "tests/integration/targets/nonlocal_group", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/nonlocal_group/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/nonlocal_group/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9b59913703c5555f1e97b9b1e8764a041a259fffcd09a24f09c855377a7db376", + "format": 1 + }, + { + "name": "tests/integration/targets/nonlocal_group/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/nonlocal_group/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d53fa307ef9f9fb57b61d947944084bfde4414a802edc15495d8bc74398029a8", + "format": 1 + }, + { + "name": "tests/integration/targets/nonlocal_user", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/nonlocal_user/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/nonlocal_user/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "db4ad7b68d18eabb6b18b54031d842dc9209e9019cb54a7ff5e8fb365568ca56", + "format": 1 + }, + { + "name": "tests/integration/targets/nonlocal_user/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/nonlocal_user/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ebafabfb587362610d62f1f31ebe26f26ffdaca31bb24e89a4cf02012871a9c3", + "format": 1 + }, + { + "name": "tests/integration/targets/protectedpaths", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/protectedpaths/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/protectedpaths/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "4b47bf5d6b384a34af40a25ce52bea6d909afd7e8d585e76a834ea57922365ef", + "format": 1 + }, + { + "name": "tests/integration/targets/protectedpaths/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/protectedpaths/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0c9ee8295aba61241af19224b49695d08f638ce618f618b6164c53b3ede75ac9", + "format": 1 + }, + { + "name": "tests/integration/targets/protectionpolicies", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/protectionpolicies/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/protectionpolicies/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3b1ecf008916ddc957db8a5fa6a8b5a125ffe6d96794c77b93b178e113cf32c5", + "format": 1 + }, + { + "name": "tests/integration/targets/protectionpolicies/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/protectionpolicies/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "95e7052734f49236fd0dca1ae91a24802868628eb02e37415c5bdca250345142", + "format": 1 + }, + { + "name": "tests/integration/targets/quotas", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/quotas/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/quotas/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "32827049dc567487858646d3b0a1abb12aeff7900f62cc4d26b0a1667c50c553", + "format": 1 + }, + { + "name": "tests/integration/targets/quotas/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/quotas/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "bff51bafa9f9dbbf82d3904e64b0b0e077d214ebe88df84e7e68c0b68b3362d7", + "format": 1 + }, + { + "name": "tests/integration/targets/s3_policy_attachment_group", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/s3_policy_attachment_group/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/s3_policy_attachment_group/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "103c555a5497b7f08e90c2d4e975dbd003fefebf0c8b44b5552eb5500cfed90a", + "format": 1 + }, + { + "name": "tests/integration/targets/s3_policy_attachment_group/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/s3_policy_attachment_group/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "095b6b4f203ff2cf824f57a3eb73d0ce0a9cc4ef332663f42de2b58f7cef6a97", + "format": 1 + }, + { + "name": "tests/integration/targets/s3_policy_attachment_user", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/s3_policy_attachment_user/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/s3_policy_attachment_user/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "adcd12d4df5028e8dba878afe63c17a6c32f6bb71ffec6f336c6204bbe70c1e0", + "format": 1 + }, + { + "name": "tests/integration/targets/s3_policy_attachment_user/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/s3_policy_attachment_user/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9f89e3ba011a4837bb88f329d29b74f172ec2194e390b3c83dc4c63c3565a121", + "format": 1 + }, + { + "name": "tests/integration/targets/s3policies", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/s3policies/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/s3policies/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0da1122cda46552d45e85ef5e869dde0547f5dee714512b7301e58a84c9618d8", + "format": 1 + }, + { + "name": "tests/integration/targets/s3policies/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/s3policies/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c9665ebf8f658c8a5a142e2417e68820b1ae5548024d5ffcf581d997c26e2c11", + "format": 1 + }, + { + "name": "tests/integration/targets/snapshots", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/snapshots/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/snapshots/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c868542c45ca03ea331f5c9cdc7856b68758fef6a0f42fd9c81d98a5d57840e3", + "format": 1 + }, + { + "name": "tests/integration/targets/snapshots/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/snapshots/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f695f6ad2391865b794d3f74628bb4a5de7f6c60863ef00a0c3411f92dcb5bfd", + "format": 1 + }, + { + "name": "tests/integration/targets/tenants", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/tenants/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/tenants/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ead062a5231124979c8d0fee27215b25d1cc8560779b2aef940d8757cb4c0964", + "format": 1 + }, + { + "name": "tests/integration/targets/tenants/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/tenants/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0a1651482f10dd4930ca0d88e6400fbbe5b50b070d5cfdcf5d5dda88faa871d3", + "format": 1 + }, + { + "name": "tests/integration/targets/user_key", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/user_key/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/user_key/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1c4403dfa03d9824621b344af3522e34c3a530b996ff88313428af2cf1d3f5a6", + "format": 1 + }, + { + "name": "tests/integration/targets/user_key/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/user_key/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d3c4c9f0c6e9e91c8b8218ca14969f0df8419e23d22e741b4aed7d0491a34849", + "format": 1 + }, + { + "name": "tests/integration/targets/users", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/users/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/users/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2a5187e084ae941360190561a4322c085742e5fea3eaaf138493e31f07876a95", + "format": 1 + }, + { + "name": "tests/integration/targets/users/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/users/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c2d275e1f1f05896ec9bdbbe90fbec6ecb6aefd9bac8a92d4a0f80ca10a565a5", + "format": 1 + }, + { + "name": "tests/integration/targets/viewpolicies", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/viewpolicies/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/viewpolicies/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ef06cf601e576e10b75d99fb3cd663b0e748c6486933a71f47e99b9bb2d163cf", + "format": 1 + }, + { + "name": "tests/integration/targets/viewpolicies/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/viewpolicies/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "303a1dd33737ffa7ab364bf6e4275da759289924d787024428b05ccb95ba0387", + "format": 1 + }, + { + "name": "tests/integration/targets/views", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/views/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/views/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2b7c474baf2a9642954d48773f64bf90eb64843e17143eee706087302e5c62bc", + "format": 1 + }, + { + "name": "tests/integration/targets/views/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/views/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a1f0e58722c8f573ff749dff4e8bcfd30664afe352667abbedd88515ac077738", + "format": 1 + }, + { + "name": "tests/integration/targets/vippools", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/vippools/defaults", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/vippools/defaults/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cffb8732e69be43987eb474b4483d41ace6d28a6bea5e3577d88c6a2b4bfaa84", + "format": 1 + }, + { + "name": "tests/integration/targets/vippools/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/vippools/tasks/main.yml", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c9ad17b4b85371d52be9563aa94ee1f60a504ce4b03e9f3626b4d307debb8f0c", + "format": 1 + }, + { + "name": "tests/unit", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/.gitkeep", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/plugins", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/plugins/module_utils/test_delete_params.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ed979727d439000a7604359542274f6c92c081a3bc3e3fb4c85a8a76096f82bd", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_dns.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "38453206cbddf2ff2f56ed0d95eda7ed9aeafbafdae8415a7ce1169c3ad16d09", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_eventdefinitionconfigs.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a4c45223afadfbd41c1755e98d5a0e32805e3eadb9b5e04882f7adad5a75dd9d", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_globalsnapstreams.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "eb8d4d57ac99d9130a842412ecdaee45646b771a7afd445274c67500aa5d7813", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_groups.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "72fc712323e35c6214c82ad8cd420a8d972bcd439ee93dc64a175d2460d80606", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_ldaps.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8c3d45b885b0cdc698233b16f49abaf75228f0894a4f7845f9ee7f905da75b90", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_nativereplicationremotetargets.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1d90dfc4555bccf7779f81c65009d30acedd3bad9fadd3fe98c4b9f7748ca9ed", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_nonlocal_group.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "882c0a8e6cd16fb5c19477741c6204f2acfc85d80bde9881beffa0f1ce9ba7f9", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_nonlocal_user.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b98eeff3e4374b6e0eafcbb131944b1f5e654a645caa362509e4719b2358bff2", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_protectedpaths.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b225f3576c9201f61a3393566688704fd1c09b8f07570d07488970649f9ad17f", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_protectionpolicies.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "efb7b699be81ea0e50318ee4145809e8926541794bb7ecb8fb996f1961ed6738", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_quotas.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "0b95d6f0da156e679ed24ade0028f8940c9801d1031e50f030e90e2e289a8b21", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_s3policies.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9149b0f057f14bd1138e60d068d1a9c373e2d9d6c2f3dfe43a1eda116ba8ee61", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_snapshots.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "68a8963de9640516536c41ab04d0ebc774eb4d7cb8a2800cc34cb40859c0ba8b", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_tenants.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "77f57c1f2b89cd3f1c2b99fd2a7f3dad881c02c30812b02da9298c6d295395b9", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_user_key.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "b313a0a601e6b54faa8002a39a44551116346a17186a5d03b2e230a07291dbda", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_users.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8723cf6c0f029007c3635c197b1351ba6c25ec3df75aeefbc70835fb39613dd6", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_viewpolicies.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "56b544fc2039cd669667743d850441979ea4263e5f27410393cea47c1600d182", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_views.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "39f45e8244f8d3aae98837c00cc6dab662f5ccae55b1748108d646e098404e07", + "format": 1 + }, + { + "name": "tests/unit/plugins/modules/test_vippools.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d635eeee4ee535459664d4c24c9aa56ac51c4acd6a282ef1099db5f4c0651d88", + "format": 1 + }, + { + "name": "CHANGELOG.md", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "ac39c8a0238c5f2bab54a2d93dbdc1c7af203d9e965cfff51ad01022c8684ef7", + "format": 1 + } + ], + "format": 1 +} \ No newline at end of file diff --git a/vendor/ansible_collections/vastdata/vms/MANIFEST.json b/vendor/ansible_collections/vastdata/vms/MANIFEST.json new file mode 100644 index 000000000..51a85522d --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/MANIFEST.json @@ -0,0 +1,34 @@ +{ + "collection_info": { + "namespace": "vastdata", + "name": "vms", + "version": "1.2.0", + "authors": [ + "VAST Data" + ], + "readme": "README.md", + "tags": [ + "storage", + "vast", + "vms" + ], + "description": "Ansible Collection for VAST VMS (VAST Management System). Manage VAST storage views, policies, and related resources.", + "license": [ + "Apache-2.0" + ], + "license_file": null, + "dependencies": {}, + "repository": "https://github.com/vast-data/vast-ansible", + "documentation": "https://github.com/vast-data/vast-ansible/blob/main/README.md", + "homepage": "https://github.com/vast-data/vast-ansible", + "issues": "https://github.com/vast-data/vast-ansible/issues" + }, + "file_manifest_file": { + "name": "FILES.json", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "24a62ea846c52560c38f1b6f10bea02921bd000e505106a4545736924e535011", + "format": 1 + }, + "format": 1 +} \ No newline at end of file diff --git a/vendor/ansible_collections/vastdata/vms/README.md b/vendor/ansible_collections/vastdata/vms/README.md new file mode 100644 index 000000000..b84005141 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/README.md @@ -0,0 +1,16 @@ +# vastdata.vms + +Ansible Collection for VAST VMS. Manage VAST views, policies, and related resources. + +## Modules + +- **vast_view**: Create, update, or delete VAST views. + +## Requirements + +- Python >= 3.9 +- requests (typically already installed with ansible-core) + +## See Also + +See the root [README](../../../../README.md) for installation and quickstart. diff --git a/vendor/ansible_collections/vastdata/vms/docs/.gitkeep b/vendor/ansible_collections/vastdata/vms/docs/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/ansible_collections/vastdata/vms/meta/runtime.yml b/vendor/ansible_collections/vastdata/vms/meta/runtime.yml new file mode 100644 index 000000000..690bd69bb --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/meta/runtime.yml @@ -0,0 +1,2 @@ +--- +requires_ansible: ">=2.19.0,<3.0.0" diff --git a/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/__init__.py b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/__init__.py new file mode 100644 index 000000000..f0488a55b --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/__init__.py @@ -0,0 +1 @@ +# Module utils for vastdata.vms collection diff --git a/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/_build_info.py b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/_build_info.py new file mode 100644 index 000000000..c2c535206 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/_build_info.py @@ -0,0 +1,2 @@ +GIT_COMMIT = "fa656c4" +GALAXY_VERSION = "1.2.0" diff --git a/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/auth.py b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/auth.py new file mode 100644 index 000000000..af91b5f9c --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/auth.py @@ -0,0 +1,43 @@ +"""Authentication validation and connection builder.""" + +from .client import VastConnection +from .errors import VastAuthError + + +def validate_auth(params: dict) -> None: + """ + Ensure token XOR (username + password) is provided. + + Raises VastAuthError if invalid. + """ + vms = params.get("vms", {}) + token = vms.get("token") + username = vms.get("username") + password = vms.get("password") + + has_token = token is not None and token != "" + has_user_pass = (username is not None and username != "") and (password is not None and password != "") + + if has_token and has_user_pass: + raise VastAuthError("Provide either token OR username+password, not both") + if not has_token and not has_user_pass: + raise VastAuthError("Provide either token OR username+password") + + +def build_connection(params: dict) -> VastConnection: + """Build VastConnection from nested vms params.""" + vms = params.get("vms") or {} + host = vms.get("host") + if not host: + raise VastAuthError("vms.host is required") + return VastConnection( + host=host, + token=vms.get("token") or None, + username=vms.get("username") or None, + password=vms.get("password") or None, + validate_certs=vms.get("validate_certs", True), + timeout=vms.get("timeout"), + tenant=vms.get("tenant") or None, + api_version=vms.get("api_version"), + debug=vms.get("debug", False), + ) diff --git a/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/client.py b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/client.py new file mode 100644 index 000000000..caee155bc --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/client.py @@ -0,0 +1,388 @@ +"""Self-contained VMS REST client. + +Replaces the external vastpy dependency with an in-house implementation +using requests. Provides the same chaining API: + + client.api.views[42].get(name="foo") + client.api.clusters.get() + client.api.users[5].access_keys.post(key="value") + +Also includes task-waiting logic (previously in waiter.py). +""" + +import json +import time +import traceback +from dataclasses import dataclass +from functools import cached_property +from typing import Any, Dict, List, Optional + +# Ansible sanity tests import all module_utils in an isolated environment +# without third-party packages. Guard the import so the module can be loaded +# for introspection; the actual check happens in VastClient.__init__. +try: + import requests + import urllib3 +except ImportError: + HAS_REQUESTS = False + REQUESTS_IMPORT_ERROR = traceback.format_exc() +else: + HAS_REQUESTS = True + REQUESTS_IMPORT_ERROR = None + +from .errors import VastAPIError + +# --------------------------------------------------------------------------- +# Build / Galaxy metadata (for User-Agent) +# --------------------------------------------------------------------------- +# _build_info.py is generated at build/test time by release_helpers.sh and contains GALAXY_VERSION and GIT_COMMIT. + +_GALAXY_VERSION = "unknown" +_GIT_COMMIT = "dev" +try: + from ._build_info import GALAXY_VERSION as _GALAXY_VERSION + from ._build_info import GIT_COMMIT as _GIT_COMMIT +except ImportError: + pass + +# --------------------------------------------------------------------------- +# Data types +# --------------------------------------------------------------------------- + + +@dataclass +class VastConnection: + """Connection parameters for VAST VMS.""" + + host: str + token: Optional[str] = None + username: Optional[str] = None + password: Optional[str] = None + validate_certs: bool = True + timeout: Optional[int] = None + tenant: Optional[str] = None + api_version: Optional[str] = None + debug: bool = False + + +class RESTFailure(VastAPIError): + """HTTP request returned a non-success status code.""" + + def __init__(self, method: str, url: str, status: int, body: str): + self.method = method + self.url = url + self.status = status + self.body = body + super().__init__(f"{method} {url} -> {status}: {body}") + + +# --------------------------------------------------------------------------- +# API path builder (replaces vastpy chaining) +# --------------------------------------------------------------------------- + +# HTTP verbs that send query-string parameters +_QUERY_VERBS = {"GET"} + + +class _APIPath: + """Lazy URL-builder that accumulates path segments. + + Every attribute access or subscript appends a segment:: + + client.api.views -> segments = ("views",) + client.api.views[42] -> segments = ("views", "42") + client.api.views[42].get -> executes GET /api//views/42/ + + Terminal methods (`get`, `post`, `patch`, `put`, `delete`) + execute the actual HTTP call. + + **GET always returns ``list[dict]``** -- if the server returns a single + object the client wraps it in a list. POST / PATCH / PUT / DELETE + return a single ``dict`` (or ``None`` for empty bodies). + """ + + __slots__ = ("_client", "_segments") + + def __init__(self, client: "VastClient", segments: tuple = ()): + object.__setattr__(self, "_client", client) + object.__setattr__(self, "_segments", segments) + + def __repr__(self) -> str: + return f"_APIPath({'/'.join(str(s) for s in self._segments)})" + + # -- path building ------------------------------------------------------- + + def __getattr__(self, part: str) -> "_APIPath": + if part.startswith("_"): + raise AttributeError(part) + return _APIPath(self._client, self._segments + (part,)) + + def __getitem__(self, part) -> "_APIPath": + return _APIPath(self._client, self._segments + (str(part),)) + + # -- terminal HTTP methods ----------------------------------------------- + + def get(self, **params) -> List[dict]: + """GET -- always returns a list of dicts.""" + result = self._client._request("GET", self._segments, params=params) + if result is None: + return [] + if isinstance(result, list): + return result + return [result] + + def post(self, **params) -> Optional[dict]: + return self._client._request("POST", self._segments, data=params) + + def patch(self, **params) -> Optional[dict]: + return self._client._request("PATCH", self._segments, data=params) + + def put(self, **params) -> Optional[dict]: + return self._client._request("PUT", self._segments, data=params) + + def delete(self, *, _query_params: Optional[dict] = None, **params) -> Optional[dict]: + return self._client._request( + "DELETE", + self._segments, + params=_query_params or None, + data=params or None, + ) + + def first(self, **params) -> Optional[dict]: + """GET and return the first result, or None if empty.""" + results = self.get(**params) + return results[0] if results else None + + +# --------------------------------------------------------------------------- +# Task-wait constants +# --------------------------------------------------------------------------- + +_TASK_SUCCESS_STATES = {"COMPLETED", "SUCCESS"} +_TASK_TERMINAL_STATES = _TASK_SUCCESS_STATES | {"FAILED", "ERROR", "CANCELLED"} + +# --------------------------------------------------------------------------- +# The client +# --------------------------------------------------------------------------- + + +class VastClient: + """Self-contained VMS REST client. + + Provides: + * Chaining REST API via ``.api`` + * Auth (token **or** username/password) + * User-Agent tracking + * Task-waiting helpers (folded from the former ``waiter.py``) + """ + + debug = False + _debug_traces: list + + def __init__(self, connection: VastConnection) -> None: + self._debug_traces = [] + if not HAS_REQUESTS: + raise RuntimeError( + "The 'requests' library is required for VastClient. " + "Install it with: pip install requests\n" + (REQUESTS_IMPORT_ERROR or "") + ) + + self._connection = connection + self._base_url = f"https://{connection.host}/api" + self._version = connection.api_version if connection.api_version is not None else "latest" + + # --- requests.Session setup ---------------------------------------- + self._session = requests.Session() + self._session.verify = connection.validate_certs + if not connection.validate_certs: + urllib3.disable_warnings(category=urllib3.exceptions.InsecureRequestWarning) + + self._session.headers["Accept"] = "application/json" + self._session.headers["Content-Type"] = "application/json" + self._session.headers["User-Agent"] = f"VastAnsible/{_GALAXY_VERSION}.{_GIT_COMMIT} {requests.utils.default_user_agent()}" + + if connection.token: + self._session.headers["Authorization"] = f"Api-Token {connection.token}" + else: + self._session.auth = (connection.username, connection.password) + + if connection.tenant: + self._session.headers["X-Tenant-Name"] = connection.tenant + + self._timeout = connection.timeout + + # -- public API ---------------------------------------------------------- + + @property + def api(self) -> _APIPath: + """Entry point for the chaining REST API.""" + return _APIPath(self) + + def pop_debug_traces(self) -> List[str]: + """Return collected debug traces and clear the buffer.""" + traces = self._debug_traces + self._debug_traces = [] + return traces + + # -- cluster resolution -------------------------------------------------- + + def _resolve_cluster(self) -> Dict[str, Any]: + return self.api.clusters.get()[0] + + @cached_property + def cluster(self) -> Dict[str, Any]: + """The cluster this client is connected to. Lazy, cached after first access.""" + return self._resolve_cluster() + + @cached_property + def is_loopback(self) -> bool: + """Whether the connected cluster is a loopback (single-node) setup.""" + return bool(self.cluster.get("loopback", False)) + + # -- low-level request --------------------------------------------------- + + def _request( + self, + method: str, + segments: tuple, + *, + params: Optional[dict] = None, + data: Optional[dict] = None, + ) -> Any: + url_parts = [self._base_url, self._version] + [str(s) for s in segments] + url = "/".join(url_parts) + "/" + + kwargs: Dict[str, Any] = {} + if self._timeout is not None: + kwargs["timeout"] = self._timeout + + def _expand_params(p: dict) -> list: + # Expand list values into repeated keys (same as vastpy) + expanded: list = [] + for k, v in p.items(): + if isinstance(v, list): + expanded.extend((k, i) for i in v) + else: + expanded.append((k, v)) + return expanded + + if method in _QUERY_VERBS: + if params: + kwargs["params"] = _expand_params(params) + else: + if data is not None: + kwargs["data"] = json.dumps(data) + + if params: + kwargs["params"] = _expand_params(params) + + if self.debug: + self._debug_traces.append(f">>> {method} {url} params={params} data={data}") + + resp = self._session.request(method, url, **kwargs) + + if self.debug: + body_preview = (resp.text or "")[:2000] + self._debug_traces.append(f"<<< {resp.status_code} ({len(resp.content or b'')}B) {body_preview}") + + try: + resp.raise_for_status() + except requests.HTTPError: + raise RESTFailure(method, url, resp.status_code, resp.text) from None + + if resp.content and "application/json" in resp.headers.get("Content-Type", ""): + return resp.json() + return None + + # -- task waiting (folded from waiter.py) -------------------------------- + + _MAX_POLL_RETRIES = 6 + + def wait_for_task( + self, + task_id: int, + timeout: int = 300, + poll_interval: int = 5, + ) -> Dict[str, Any]: + """Poll a VMS async task until it reaches a terminal state. + + Tolerates transient connection errors (e.g. ConnectionResetError) + that occur when VMS restarts services during cnode enable/disable. + + Returns the final task dict on success. + Raises VastAPIError on failure or timeout. + """ + start = time.time() + last_state = None + consecutive_errors = 0 + + while True: + if time.time() - start >= timeout: + raise VastAPIError(f"Task {task_id} timed out after {timeout}s. Last state: {last_state}") + + try: + task = self._get_task(task_id) + consecutive_errors = 0 + except VastAPIError: + consecutive_errors += 1 + if consecutive_errors >= self._MAX_POLL_RETRIES: + raise + time.sleep(poll_interval) + continue + + state = task.get("state", task.get("status", "UNKNOWN")) + last_state = state + + if state in _TASK_SUCCESS_STATES: + return task + + if state in _TASK_TERMINAL_STATES: + error = task.get("failure_reason") or task.get("error") or task.get("message") + if not error: + messages = task.get("messages") or [] + error = messages[-1] if messages else "Unknown error" + raise VastAPIError(f"Task {task_id} failed ({state}): {error}") + + time.sleep(poll_interval) + + @staticmethod + def extract_task_id(response: Dict[str, Any]) -> Optional[int]: + """Extract task ID from an async API response. + + Checks several common response shapes used by VMS. + Returns None if no task ID is found. + """ + if not response: + return None + + if "task_id" in response: + return response["task_id"] + + async_task = response.get("async_task") or {} + task_id = async_task.get("id") or async_task.get("task_id") + if task_id: + return task_id + + if "id" in response and response.get("type") == "async_task": + return response["id"] + + return None + + # -- internal helpers ---------------------------------------------------- + + def _get_task(self, task_id: int) -> Dict[str, Any]: + """Fetch task status from the vtasks endpoint.""" + try: + task = self.api.vtasks.first(id=task_id) + if task: + return task + + task = self.api.async_tasks.first(id=task_id) + if task: + return task + + raise VastAPIError(f"Task {task_id} not found") + except VastAPIError: + raise + except Exception as e: + raise VastAPIError(f"Failed to get task {task_id}: {e}") from e diff --git a/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/diff.py b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/diff.py new file mode 100644 index 000000000..fa15554e3 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/diff.py @@ -0,0 +1,185 @@ +"""Compute minimal patch between current and desired state. + +Provides functions for normalizing resources and computing patches, +with support for read-only field filtering and set-like list comparison. +""" + +from typing import Any, Dict, Optional + + +def normalize_value(value: Any, set_like: bool = False) -> Any: + """Normalize a value for comparison. + + Args: + value: The value to normalize. + set_like: If True, treat lists as sets (sort and dedupe). + + Returns: + Normalized value. + """ + if value is None: + return None + + if set_like and isinstance(value, list): + # Sort and dedupe for order-insensitive comparison + try: + return sorted(set(value)) + except TypeError: + # Items not sortable, just dedupe + seen = [] + for item in value: + if item not in seen: + seen.append(item) + return seen + + return value + + +def normalize_resource( + resource: Dict[str, Any], + overrides: Dict[str, Any], + exclude_immutable: bool = False, + include_ephemeral: bool = False, + user_resource: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + """Normalize a resource for comparison. + + Excludes read-only, optionally ephemeral, and optionally immutable fields; + normalizes set-like lists and applies field-specific normalizers. + + Args: + resource: The resource dictionary to normalize. + overrides: Schema overrides containing read_only_fields, ephemeral_fields, + immutable_fields, set_like_lists, and field_normalizers. + exclude_immutable: If True, also exclude immutable fields (for update diffs). + include_ephemeral: If True, keep ephemeral fields (e.g. password) in the result. + user_resource: Optional user-provided resource (desired state) to pass to + field normalizers. Normalizers can use this to extract schema + from user input. + + Returns: + Normalized resource dictionary. + """ + if not resource: + return {} + + read_only = overrides.get("read_only_fields", set()) + ephemeral = overrides.get("ephemeral_fields", set()) + immutable = overrides.get("immutable_fields", set()) if exclude_immutable else set() + set_like = overrides.get("set_like_lists", set()) + field_normalizers = overrides.get("field_normalizers", {}) + + result = {} + for key, value in resource.items(): + # Skip read-only fields + if key in read_only: + continue + + # Skip ephemeral fields unless include_ephemeral (current state never has them; desired keeps them for patch) + if not include_ephemeral and key in ephemeral: + continue + + # Skip immutable fields when computing update diffs + if key in immutable: + continue + + # Apply field-specific normalizer if defined + if key in field_normalizers: + # Pass both API value and user value to normalizer + user_value = user_resource.get(key) if user_resource else None + value = field_normalizers[key](value, user_value) + + # Normalize the value + is_set_like = key in set_like + result[key] = normalize_value(value, set_like=is_set_like) + + return result + + +def values_equal(current_val: Any, desired_val: Any, set_like: bool = False) -> bool: + """Compare two values for equality. + + Args: + current_val: Current value. + desired_val: Desired value. + set_like: If True, compare lists as sets. + + Returns: + True if values are equal. + """ + if current_val is None and desired_val is None: + return True + + # Some APIs omit boolean fields when their value is False (absent == False). + # Treat a missing field (None) as equal to False to avoid spurious patches. + if desired_val is False and current_val is None: + return True + + if set_like and isinstance(current_val, list) and isinstance(desired_val, list): + # Compare as sets + try: + return set(current_val) == set(desired_val) + except TypeError: + # Items not hashable, fall back to sorted comparison + pass + + return current_val == desired_val + + +def compute_patch( + current: Dict[str, Any], + desired: Dict[str, Any], + overrides: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + """Compute minimal dict patch from current to desired. + + Args: + current: Current resource state (should be normalized to exclude read-only/ephemeral fields). + desired: Desired resource state (should be normalized to exclude read-only/ephemeral fields). + overrides: Schema overrides for set-like lists. If None, uses simple comparison. + + Returns: + Dict suitable for PATCH request, containing only changed values. + + Notes: + - Only includes keys present in desired. + - Treats None as "not provided" (omit from patch). + - Respects set_like_lists from overrides for order-insensitive comparison. + - Ephemeral fields (e.g. passwords) are excluded from patches to maintain idempotency, + since they are never returned by the API and cannot be verified for changes. + """ + if overrides is None: + overrides = {} + + set_like_lists = overrides.get("set_like_lists", set()) + + patch: Dict[str, Any] = {} + for key, desired_val in desired.items(): + if desired_val is None: + continue + + current_val = current.get(key) + is_set_like = key in set_like_lists + + if not values_equal(current_val, desired_val, set_like=is_set_like): + patch[key] = desired_val + + return patch + + +def has_changes( + current: Dict[str, Any], + desired: Dict[str, Any], + overrides: Optional[Dict[str, Any]] = None, +) -> bool: + """Check if there are any changes between current and desired state. + + Args: + current: Current resource state. + desired: Desired resource state. + overrides: Schema overrides. + + Returns: + True if there are changes. + """ + return bool(compute_patch(current, desired, overrides)) diff --git a/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/errors.py b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/errors.py new file mode 100644 index 000000000..cf9195670 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/errors.py @@ -0,0 +1,25 @@ +"""Exception classes for vastdata.vms collection.""" + + +class VastError(RuntimeError): + """Base exception for VAST-related errors.""" + + pass + + +class VastAuthError(VastError): + """Authentication or authorization error.""" + + pass + + +class VastNotFoundError(VastError): + """Resource not found.""" + + pass + + +class VastAPIError(VastError): + """API or network error.""" + + pass diff --git a/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/module_customizations.py b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/module_customizations.py new file mode 100644 index 000000000..894376735 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/module_customizations.py @@ -0,0 +1,38 @@ +"""Registry of modules with manual customizations outside AUTOGENERATED blocks. + +When a module needs code that the generator cannot produce (e.g. a custom +``validate_run_params`` override), the custom code is placed *between* +AUTOGENERATED marker pairs -- the generator only replaces content *inside* +the markers, so the custom code survives regeneration. + +This registry declares which modules carry such customizations and what +string markers to look for. The code generator checks this after writing +a module file; if expected markers are missing it fails loudly instead of +silently losing the custom code. +""" + +CUSTOMIZED_MODULES = { + "cnodes": { + "description": ( + "CNode-specific validate_run_params override: allows creation " + "by ip (instead of name/id) and loopback auto-discovery." + ), + "markers": [ + "_apply_cnode_customizations()", + "CnodeResource.validate_run_params = _cnode_validate_run_params", + ], + }, + "user_key": { + "description": ( + "UserKey-specific run/create/update/get_current_key overrides: " + "the /users/{id}/access_keys/ endpoint does not support GET, so " + "current key state is fetched from the user object to drive " + "idempotent POST/PATCH. the lookup uses GET /users/query/ to see " + "keys in any tenant; otherwise it falls back to /users/{id}/." + ), + "markers": [ + "_apply_user_key_customizations()", + "UserKey.run = _user_key_run", + ], + }, +} diff --git a/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/protectionpolicies_utils.py b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/protectionpolicies_utils.py new file mode 100644 index 000000000..d95434ec7 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/protectionpolicies_utils.py @@ -0,0 +1,76 @@ +"""Helpers for the ``protectionpolicies`` Ansible module.""" + +import re + + +class Duration(float): + NAMED_UNITS = dict( + ms=1 / 1000, + s=1, + S=1, + m=60, + h=60 * 60, + H=60 * 60, + d=24 * 60 * 60, + D=24 * 60 * 60, + w=7 * 24 * 60 * 60, + W=7 * 24 * 60 * 60, + M=30 * 24 * 60 * 60, + y=365 * 24 * 60 * 60, + Y=365 * 24 * 60 * 60, + ) + + def __new__(cls, value): + if isinstance(value, str): + try: + i, u = float(value), None + except ValueError: + i, u = re.match(r"(\d*(?:\.\d+)?)?(\w*)", value).groups() + i = 1.0 if not i else float(i) + value = i * (cls.NAMED_UNITS[u] if u else 1) + if isinstance(value, cls): + return value + + return super().__new__(cls, value) + + +_FRAME_DURATION_KEYS = {"every", "keep-local", "keep-remote"} + + +def _equivalent_duration(a, b): + """Return True iff two duration strings represent the same length of time.""" + if not isinstance(a, str) or not isinstance(b, str): + return False + try: + return Duration(a) == Duration(b) + except (KeyError, ValueError, AttributeError): + return False + + +def normalize_frames(api_value, user_value): + """Normalize API frames against user frames so equivalent durations don't diff.""" + if api_value is None or user_value is None: + return api_value + if not isinstance(api_value, list) or not isinstance(user_value, list): + return api_value + if len(api_value) != len(user_value): + return api_value + + normalized = [] + for api_frame, user_frame in zip(api_value, user_value): + if not isinstance(api_frame, dict) or not isinstance(user_frame, dict): + normalized.append(api_frame) + continue + + new_frame = {} + for key, user_v in user_frame.items(): + if key not in api_frame: + continue + api_v = api_frame[key] + if key in _FRAME_DURATION_KEYS and _equivalent_duration(api_v, user_v): + new_frame[key] = user_v + else: + new_frame[key] = api_v + normalized.append(new_frame) + + return normalized diff --git a/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/resource.py b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/resource.py new file mode 100644 index 000000000..76a3c7eba --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/resource.py @@ -0,0 +1,603 @@ +"""Base Resource class for Ansible modules. + +Provides common CRUD functionality for all resource modules, +eliminating code duplication across 100+ generated modules. +""" + +import re +from typing import Any, Dict, Optional, Set + +from ansible.module_utils.basic import AnsibleModule + +from .auth import build_connection, validate_auth +from .client import VastClient +from .diff import compute_patch, normalize_resource +from .errors import VastAPIError +from .schema_overrides import get_overrides +from .timeouts import DEFAULT_TASK_TIMEOUT +from .version import ensure_supported_version + + +class BaseResource: + """Base class for all Ansible module resources. + + Provides common CRUD operations and idempotent lifecycle management. + + Subclasses must define: + - resource_name: str # e.g., "alarms", "views" + - singular: str # e.g., "alarm", "view" + - lookup_field: str # e.g., "name", "path" + + Subclasses may override: + - async_create: bool = False + - async_update: bool = False + - async_delete: bool = False + - include_ephemeral_in_updates: bool = False # Set True to allow password updates (breaks idempotency) + - get(), create(), update(), delete() methods for custom behavior + """ + + resource_name: str = NotImplemented + singular: str = NotImplemented + lookup_field: str = "name" + + # Override these for async resources + async_create: bool = False + async_update: bool = False + async_delete: bool = False + + # Override this to include ephemeral fields (passwords/secrets) in update patches + # This breaks idempotency but allows password updates. Default False for idempotency. + include_ephemeral_in_updates: bool = False + + # Swagger-derived field restrictions (auto-generated by code generator) + create_only_fields: Set[str] = set() # Fields only valid during creation (excluded from updates) + update_only_fields: Set[str] = set() # Fields only valid during updates (excluded from creates) + delete_query_params: Set[str] = set() + delete_body_params: Set[str] = set() + + def __init__(self, module: AnsibleModule): + """Initialize the resource manager. + + Args: + module: AnsibleModule instance + """ + self.module = module + self.params = module.params + self.check_mode = module.check_mode + + # Validate authentication + try: + validate_auth(self.params) + except Exception as e: + module.fail_json(msg=str(e)) + + # Build connection and client + conn = build_connection(self.params) + try: + self.client = VastClient(conn) + except RuntimeError as e: + module.fail_json(msg=str(e)) + + self.client.debug = conn.debug + + # Validate product version + ensure_supported_version(module, self.client, min_version=(5, 3, 0), max_version=(5, 5, 0)) + + # Get schema overrides for this resource + self.overrides = get_overrides(self.resource_name) + + # -- debug trace helpers ------------------------------------------------- + + _MAX_DEBUG_TRACES = 20 + + _AUTH_HEADER_RE = re.compile( + r"(Authorization:\s*(?:Bearer|Basic|Api-Token)\s+)\S+", + re.IGNORECASE, + ) + _TOKEN_QUERY_RE = re.compile( + r"([\?&](?:token|api_key|access_token)=)[^&\s]+", + re.IGNORECASE, + ) + + def _emit_debug_traces(self) -> None: + """Emit collected HTTP debug traces as Ansible warnings. + + Uses ``pop_debug_traces()`` so the buffer is cleared after retrieval. + Only the last ``_MAX_DEBUG_TRACES`` entries are shown; earlier ones are + noted with a count. + """ + if not self.client.debug: + return + traces = self.client.pop_debug_traces() + if not traces: + return + if len(traces) > self._MAX_DEBUG_TRACES: + self.module.warn(f"[VAST HTTP] ({len(traces) - self._MAX_DEBUG_TRACES} earlier traces omitted)") + for trace in traces[-self._MAX_DEBUG_TRACES :]: + self.module.warn(f"[VAST HTTP] {self._sanitize_trace(trace)}") + + def _sanitize_trace(self, trace: str) -> str: + """Scrub secrets from a debug trace string.""" + conn = self.client._connection + for secret in (conn.password, conn.token): + if secret: + trace = trace.replace(secret, "***") + trace = self._AUTH_HEADER_RE.sub(r"\1***", trace) + trace = self._TOKEN_QUERY_RE.sub(r"\1***", trace) + return trace + + # -- field helpers ------------------------------------------------------- + + def _get_field_value(self, resource: Dict[str, Any], field_name: str) -> Any: + """Get field value from resource, handling nested objects. + + The API often returns nested objects (e.g., local_provider: {id: 1, name: "default"}), + but module parameters use flat IDs (e.g., local_provider_id: 1). + This function handles both cases. + """ + # Try direct access first + value = resource.get(field_name) + if value is not None: + return value + + # If field ends with _id, try nested object access + # e.g., local_provider_id -> local_provider.id + if field_name.endswith("_id"): + nested_field = field_name[:-3] # Remove "_id" suffix + nested_obj = resource.get(nested_field) + if isinstance(nested_obj, dict): + return nested_obj.get("id") + # Handle case where nested object is already an int + if isinstance(nested_obj, int): + return nested_obj + + return None + + def _is_not_found_error(self, exception: Exception) -> bool: + """Check if exception looks like a 404 Not Found error. + + Args: + exception: Exception from API call + + Returns: + True if this looks like a not-found error, False otherwise + """ + # Check common patterns in exception messages + err_str = str(exception).lower() + return any( + pattern in err_str + for pattern in [ + "404", + "not found", + "does not exist", + "no such", + "resource not found", + ] + ) + + def _needs_detail_fetch(self, list_result: Dict[str, Any]) -> bool: + """Check if user wants fields that are missing from list result. + + Compares user's desired state with list endpoint result to determine + if a detail endpoint fetch is needed to get complete data. + + Args: + list_result: Resource dict from list endpoint + + Returns: + True if detail fetch needed, False if list result is sufficient + + Example: + User wants: {path: "/foo", bucket_logging: {...}} + List has: {path: "/foo"} + Missing: bucket_logging + → Returns True (refetch needed) + """ + if "id" not in list_result: + return False # Can't fetch detail without ID + + # Get user's desired state + desired = self.build_desired_state() + if not desired: + return False # No desired state, list result is fine + + # Check if user wants any fields that list doesn't have + list_keys = set(list_result.keys()) + desired_keys = set(desired.keys()) + + missing_fields = desired_keys - list_keys + + # If user wants fields not in list result, we need detail fetch + return len(missing_fields) > 0 + + def get( + self, + lookup_value: Optional[str] = None, + resource_id: Optional[int] = None, + unique_constraints: Optional[Dict[str, Any]] = None, + ) -> Optional[Dict[str, Any]]: + """Get resource by lookup field or ID, with optional composite key filtering. + + Args: + lookup_value: Value of the lookup field (e.g., name="foo") + resource_id: Optional ID for direct lookup (bypasses lookup field search) + unique_constraints: Optional dict of fields that together uniquely identify the resource + + Returns: + Resource dict if found, None otherwise + + Raises: + VastAPIError: On API errors (except 404/not-found) + + Note: If unique_constraints is provided, multiple matches are filtered to find the exact resource. + """ + try: + api = self.client.api[self.resource_name] + + # If ID provided, use direct ID lookup (most reliable) + if resource_id is not None: + try: + return api[resource_id].first() + except Exception as e: + # Only treat genuine not-found as None; re-raise auth/connection errors + if self._is_not_found_error(e): + return None + raise + + # If unique constraints are provided and lookup_field is not part of them, + # search by unique constraints directly (enables rename operations) + if unique_constraints and self.lookup_field not in unique_constraints: + results = api.get(**unique_constraints) + if len(results) == 1: + return results[0] + if len(results) > 1: + self.module.fail_json( + msg=(f"Multiple {self.singular} resources found with " f"unique_constraints={unique_constraints}") + ) + # len(results) == 0: no resource matches, fall through to lookup_field search + + # Try lookup field-based lookup + if lookup_value: + results = api.get(**{self.lookup_field: lookup_value}) + + # Apply unique constraint filtering if specified + if unique_constraints and results: + matches = [r for r in results if all(self._get_field_value(r, k) == v for k, v in unique_constraints.items())] + if len(matches) == 1: + resource = matches[0] + elif len(matches) > 1: + self.module.fail_json( + msg=( + f"Multiple {self.singular} resources found with " + f"{self.lookup_field}='{lookup_value}' and unique_constraints={unique_constraints}" + ) + ) + else: + # len(matches) == 0: no resource matches the unique constraints + return None + elif results: + # No constraints: use first result (backward compatible) + resource = results[0] + else: + return None + + # Smart refetch: Only if user wants fields missing from list result + if self._needs_detail_fetch(resource): + try: + return api[resource["id"]].first() or resource + except Exception: + # If detail fetch fails, return list result + return resource + + return resource + + return None + except Exception as e: + raise VastAPIError( + f"Failed to get {self.singular} (lookup_field={self.lookup_field}, lookup_value={lookup_value}, id={resource_id}): {e}" + ) from e + + def create(self, payload: Dict[str, Any]) -> Dict[str, Any]: + """Create a new resource. + + Args: + payload: Resource data + + Returns: + Created resource dict + + Raises: + VastAPIError: On API errors + """ + try: + api = self.client.api[self.resource_name] + result = api.post(**payload) + except Exception as e: + raise VastAPIError(f"Failed to create {self.singular}: {e}") from e + + # Wait for async create if needed + if self.async_create and self.params.get("wait", True): + self._wait_for_task(result) + # Refresh to get actual resource data (not task object) + if "id" in result and result.get("type") != "async_task": + refreshed = self.get(resource_id=result["id"]) + if refreshed: + return refreshed + + return result + + def update(self, resource_id: int, patch: Dict[str, Any]) -> Dict[str, Any]: + """Update an existing resource. + + Args: + resource_id: Resource ID + patch: Fields to update + + Returns: + Updated resource dict + + Raises: + VastAPIError: On API errors + """ + try: + api = self.client.api[self.resource_name] + result = api[resource_id].patch(**patch) + except Exception as e: + raise VastAPIError(f"Failed to update {self.singular} {resource_id}: {e}") from e + + # Wait for async update if needed + if self.async_update and self.params.get("wait", True): + self._wait_for_task(result) + # Refresh to get actual resource data (not task object) + refreshed = self.get(resource_id=resource_id) + if refreshed: + return refreshed + + return result + + def delete( + self, + resource_id: int, + query_params: Optional[Dict[str, Any]] = None, + body_params: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """Delete a resource. + + Args: + resource_id: Resource ID + + Returns: + Delete response (may contain task_id for async) + + Raises: + VastAPIError: On API errors + """ + try: + api = self.client.api[self.resource_name] + kwargs: Dict[str, Any] = dict(body_params or {}) + if query_params: + kwargs["_query_params"] = query_params + result = api[resource_id].delete(**kwargs) + result = result if result else {} + except Exception as e: + raise VastAPIError(f"Failed to delete {self.singular} {resource_id}: {e}") from e + + # Wait for async delete if needed + if self.async_delete and self.params.get("wait", True): + self._wait_for_task(result) + + return result + + def _collect_delete_params(self) -> Dict[str, Dict[str, Any]]: + """Collect user-supplied DELETE-only param values from ``self.params``.""" + + def _pick(names: Set[str]) -> Dict[str, Any]: + return {n: v for n in names for v in [self.params.get(n)] if v is not None} + + return {"query": _pick(self.delete_query_params), "body": _pick(self.delete_body_params)} + + def build_desired_state(self, operation: str = "update", current_state: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Build desired state from module parameters. + + Excludes connection parameters, framework fields, and operation-specific fields. + + Args: + operation: One of "create" or "update" - determines which fields to include + current_state: Current resource state (for smart update_only_fields handling) + + Returns: + Dict of resource fields from module parameters + """ + # Exclude framework parameters (connection, state, wait, etc.) + exclude_keys = { + "vms", # Connection parameters are nested in this dict + "id", # ID is for lookup only, not part of desired state + "state", + "wait", + "wait_timeout", + "query", + # Special operation parameters (actions, not resource fields) + "set_posix_primary", + "revoke", + "revoke_access_keys", + } + + exclude_keys.update(self.delete_query_params) + exclude_keys.update(self.delete_body_params) + + # Add operation-specific exclusions based on Swagger spec + if operation == "create": + # During create, exclude update-only fields (e.g., s3policies.enabled) + # These fields cannot be set during creation per Swagger spec + exclude_keys.update(self.update_only_fields) + elif operation == "update": + # During update, exclude create-only fields (can't be changed after creation) + exclude_keys.update(self.create_only_fields) + + desired = {} + for key, value in self.params.items(): + if key not in exclude_keys and value is not None: + desired[key] = value + + return desired + + def validate_run_params(self) -> None: + """Validate parameters before run(). + + Ensures we have a way to identify the resource for state=present. + Subclasses can override for custom validation (e.g. CnodeResource + relaxes this for loopback clusters). + + Raises: + VastAPIError: If required parameters are missing. + """ + state = self.params.get("state", "present") + lookup_value = self.params.get(self.lookup_field) + resource_id = self.params.get("id") + if not lookup_value and not resource_id and state == "present": + raise VastAPIError(f"Either '{self.lookup_field}' or 'id' parameter is required when state=present") + + def run(self) -> None: + """Main execution entry point. + + Wraps ``_run_lifecycle()`` so that any ``VastAPIError`` is caught, + debug traces are emitted (on failure only), and ``fail_json`` is + called exactly once at the boundary. + """ + try: + self._run_lifecycle() + except VastAPIError as e: + self._emit_debug_traces() + self.module.fail_json(msg=str(e)) + + def _run_lifecycle(self) -> None: + """Full CRUD lifecycle -- called by ``run()``. + + All errors raise ``VastAPIError``; the caller handles ``fail_json``. + """ + # Check for special operation parameters (BaseResource only supports CRUD operations) + special_ops = { + "set_posix_primary", + "revoke", + "revoke_access_keys", + } + active_special_ops = [op for op in special_ops if self.params.get(op)] + + if active_special_ops: + if self.check_mode: + self.module.exit_json( + changed=True, + msg=f"Check mode: Would perform special operation(s): {', '.join(active_special_ops)}", + **{self.resource_name: {}}, + ) + raise VastAPIError( + f"Special operations ({', '.join(active_special_ops)}) require custom module implementation. " + f"The BaseResource class only supports standard CRUD operations (create, read, update, delete)." + ) + + state = self.params.get("state", "present") + lookup_value = self.params.get(self.lookup_field) + resource_id = self.params.get("id") + + self.validate_run_params() + + # Build unique constraints dict from params for composite key lookups + unique_constraint_fields = self.overrides.get("unique_constraints", set()) + unique_constraints = None + if unique_constraint_fields: + unique_constraints = {k: self.params.get(k) for k in unique_constraint_fields if self.params.get(k) is not None} + if unique_constraints and lookup_value and self.lookup_field in unique_constraint_fields: + if self.lookup_field not in unique_constraints: + unique_constraints[self.lookup_field] = lookup_value + + # Get current state + current = ( + self.get(lookup_value, resource_id, unique_constraints=unique_constraints) if (lookup_value or resource_id) else None + ) + + changed = False + result_data = {} + diff_before = {} + diff_after = {} + + if state == "absent": + if current: + if not self.check_mode: + delete_kwargs = self._collect_delete_params() + self.delete( + current["id"], + query_params=delete_kwargs["query"], + body_params=delete_kwargs["body"], + ) + changed = True + diff_before = dict(current) + diff_after = {} + result_data = current or {} + + else: # state == present + desired = self.build_desired_state(operation="create" if not current else "update", current_state=current) + + if not current: + if self.update_only_fields: + provided_update_only = [f for f in self.update_only_fields if f in self.params and self.params[f] is not None] + if provided_update_only: + self.module.warn( + f"Fields {provided_update_only} are update-only per API spec and will be ignored during creation. " + f"The resource will be created without these fields. " + f"To set them, run a separate update task after creation." + ) + if not self.check_mode: + result_data = self.create(desired) + else: + result_data = desired + changed = True + diff_before = {} + diff_after = dict(result_data) + else: + current_normalized = normalize_resource(current, self.overrides, exclude_immutable=True, user_resource=desired) + desired_normalized = normalize_resource( + desired, + self.overrides, + exclude_immutable=True, + include_ephemeral=self.include_ephemeral_in_updates, + ) + patch = compute_patch(current_normalized, desired_normalized, self.overrides) + + if patch: + if not self.check_mode: + result_data = self.update(current["id"], patch) + else: + result_data = {**current, **patch} + changed = True + diff_before = dict(current) + diff_after = dict(result_data) + else: + result_data = current + + # Build result + result = { + "changed": changed, + self.resource_name: result_data, + } + if changed and (diff_before or diff_after): + result["diff"] = {"before": diff_before, "after": diff_after} + + self.module.exit_json(**result) + + def _wait_for_task(self, response: Dict[str, Any]) -> None: + """Wait for an async task to complete. + + Args: + response: API response that may contain task_id + + Raises: + VastAPIError: If task fails or times out. + """ + if response is None: + return + task_id = VastClient.extract_task_id(response) + if task_id: + try: + self.client.wait_for_task(task_id, timeout=self.params.get("wait_timeout", DEFAULT_TASK_TIMEOUT)) + except VastAPIError as e: + raise VastAPIError(f"Async task {task_id} failed: {str(e)}") from e diff --git a/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/s3_attachment_utils.py b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/s3_attachment_utils.py new file mode 100644 index 000000000..860e9d3e4 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/s3_attachment_utils.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +"""Shared helpers for S3 policy attachment modules (user/group).""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +def resolve_s3_policy_id_by_guid(client, policy_guid): + if not policy_guid: + return None + result = client.api.s3policies.get() + if not result: + return None + for p in result: + if p and p.get("guid") == policy_guid: + return p.get("id") + return None + + +def current_policy_ids(entity): + raw = entity.get("s3_policies_ids") if entity else None + if raw is None: + return [] + if isinstance(raw, (list, dict)): + return [int(x) for x in raw if x is not None] + return [] diff --git a/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/schema_overrides.py b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/schema_overrides.py new file mode 100644 index 000000000..68d596a58 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/schema_overrides.py @@ -0,0 +1,1913 @@ +"""Schema overrides for VAST resources. + +This file uses a hybrid model for resource field classifications: + +- read_only_fields: **Auto-populated by the generator** from Swagger analysis + (fields absent from create/update params). Updated on every generation run + via update_schema_overrides() in generate_from_swagger.py. +- immutable_fields: Manually maintained - fields that cannot be changed after creation +- ephemeral_fields: Manually maintained - write-only fields never returned by API +- set_like_lists: Manually maintained - list fields where order doesn't matter +- lookup_field: Manually maintained - canonical identifier for idempotency lookups +- field_normalizers: Manually maintained - functions to normalize field values before comparison + +The default_read_only fallback has been removed. All 79 resources have explicit +entries with auto-generated read_only_fields. Unknown resources get empty sets. + +Note: Fields missing from list endpoints are automatically handled - the system +refetches from detail endpoint when user provides fields not present in list result. + +These overrides are consulted by the module generator and at runtime for proper +idempotent behavior. +""" + +from typing import Any, Dict, Set + +from .protectionpolicies_utils import normalize_frames + + +def normalize_list_by_user_schema(api_value: Any, user_value: Any) -> Any: + """Generic normalizer for list fields that keeps only user-provided keys. + + Args: + api_value: List value returned by API (may have extra fields in each dict) + user_value: List value provided by user (defines the schema) + + Returns: + Normalized list with only user-provided keys, flattening nested 'entity' objects + + Example: + User provides: [{"identifier": "user1", "hard_limit": 1000}] + API returns: [{"entity": {"identifier": "user1", "vast_id": 123}, "hard_limit": 1000, "state": "OK"}] + Returns: [{"identifier": "user1", "hard_limit": 1000}] + """ + if api_value is None or user_value is None: + return api_value + if not isinstance(api_value, list): + return api_value + if len(user_value) == 0: + return api_value + + user_schema = user_value[0] + + normalized = [] + for api_item in api_value: + normalized_item = {} + for key in user_schema.keys(): + try: + if key in api_item: + value = api_item[key] + elif "entity" in api_item and key in api_item["entity"]: + entity_value = api_item["entity"][key] + # The entity sub-object is the API's read-only view of the + # referenced user/group profile. Fields like identifier and + # identifier_type are reliably echoed, but others (e.g. email) + # may carry different semantics than the flat field the user + # sent. When the entity value disagrees with what the user + # provided, the field is unverifiable through GET — treat it + # like an ephemeral/write-only field by adopting the user's + # value so both sides of the comparison stay consistent. + if entity_value != user_schema.get(key): + value = user_schema[key] + else: + value = entity_value + else: + continue + + # Normalize "All" → "" transformation for fqdn fields (share_acl) + if key == "fqdn" and value == "" and user_schema.get(key) == "All": + value = "All" + + if value is not None: + normalized_item[key] = value + except (TypeError, KeyError): + continue + + normalized.append(normalized_item) + + return normalized + + +def normalize_dict_by_user_schema(api_value: Any, user_value: Any) -> Any: + """Generic normalizer for dict fields that keeps only user-provided keys. + + Args: + api_value: Dict value returned by API (may have extra fields) + user_value: Dict value provided by user (defines the schema) + + Returns: + Normalized dict with only user-provided keys + + Example: + User provides: {"soft_limit": 50000, "hard_limit": 100000} + API returns: {"soft_limit": 50000, "hard_limit": 100000, "quota_system_id": 152, "grace_period": null} + Returns: {"soft_limit": 50000, "hard_limit": 100000} + """ + if api_value is None or user_value is None: + return api_value + if not isinstance(api_value, dict) or not isinstance(user_value, dict): + return api_value + + normalized = {} + for key in user_value.keys(): + if key in api_value and api_value[key] is not None: + normalized[key] = api_value[key] + + return normalized + + +def normalize_share_acl(api_value: Any, user_value: Any) -> Any: + """Normalize share_acl field (wrapper for nested acl list). + + Args: + api_value: share_acl dict from API with nested 'acl' list + user_value: share_acl dict from user with nested 'acl' list + + Returns: + Normalized share_acl with only user-provided keys in acl entries + """ + if not user_value or "acl" not in user_value: + return api_value + + # Normalize the nested 'acl' list using generic list normalizer + if "acl" in api_value: + normalized_acl = normalize_list_by_user_schema(api_value["acl"], user_value["acl"]) + return {**api_value, "acl": normalized_acl} + + return api_value + + +# Resource-specific overrides +OVERRIDES: Dict[str, Dict[str, Any]] = { + "views": { + "read_only_fields": { + "bulk_permission_update_progress", + "bulk_permission_update_state", + "cluster", + "cluster_id", + "created", + "directory", + "guid", + "has_bucket_logging_destination", + "has_bucket_logging_sources", + "id", + "ignore_oos", + "internal", + "is_remote", + "logical_capacity", + "nqn", + "physical_capacity", + "policy", + "sync", + "sync_time", + "tenant_name", + "title", + "url", + }, + "immutable_fields": {"path", "tenant_id"}, + "ephemeral_fields": {"create_dir"}, + "set_like_lists": {"protocols", "abac_tags", "abe_protocols"}, + "lookup_field": "path", + "field_normalizers": { + "share_acl": normalize_share_acl, # Generic normalizer handles 'All' → '' and API-added fields + }, + }, + "users": { + "read_only_fields": { + "access_keys", + "group_count", + "groups", + "guid", + "id", + "is_temporary_password", + "leading_group_gid", + "leading_group_name", + "local_provider", + "password_is_set", + "primary_group_sid", + "provider_name", + "s3_policies", + "sid", + "sids", + "title", + "url", + "vid", + }, + "immutable_fields": { + "name", + "local_provider_id", + }, # Username cannot be changed, local_provider_id is write-once at creation + "ephemeral_fields": { + "password", # Never returned by API; only sent on create (excluded from updates for idempotency). + }, + "set_like_lists": {"gids"}, + "unique_constraints": {"name", "local_provider_id"}, # Users are uniquely identified by (name, local_provider_id) + "lookup_field": "name", + }, + "clusters": { + "read_only_fields": { + "active_sessions_count", + "allow_encryption", + "auxiliary_space_in_use", + "auxiliary_space_in_use_tb", + "available_logical_capacity", + "available_physical_capacity", + "available_upgrade_version", + "bw", + "bw_mb", + "created", + "current_gen_enabled", + "deployment_time", + "drive_pci_port_type", + "drr", + "drr_text", + "enable_s3", + "encryption_status", + "encryption_transition_state", + "estore_capacity_in_use_bytes", + "estore_capacity_in_use_tb", + "expansion_phase", + "expansion_phase_description", + "expansion_state", + "free_logical_space", + "free_logical_space_tb", + "free_physical_space", + "free_physical_space_tb", + "free_physical_space_wo_overhead", + "free_physical_space_wo_overhead_tb", + "free_usable_capacity", + "free_usable_capacity_tb", + "guid", + "id", + "iops", + "ip", + "is_large_subnet", + "is_wb_raid_enabled", + "latency", + "latency_ms", + "leader_cnode", + "leader_state", + "leader_upgrade_state", + "logical_auxiliary_space_in_use", + "logical_auxiliary_space_in_use_tb", + "logical_capacity", + "logical_drr_percent", + "logical_inodes_in_use_num", + "logical_space", + "logical_space_in_use", + "logical_space_in_use_percent", + "logical_space_in_use_tb", + "logical_space_tb", + "max_handles_count", + "max_performance", + "max_performance_metrics", + "md_iops", + "md_usage_health", + "memory_raid_rebuild_progress", + "memory_raid_state", + "mgmt_cnode", + "mgmt_inner_vip", + "mgmt_inner_vip_cnode", + "mgmt_vip", + "micro_estore_shards", + "mio_raid_state", + "ndb_bandwidth", + "ndb_bandwidth_read", + "ndb_bandwidth_write", + "ndb_number_of_running_queries", + "ndb_rows_scanned_per_second", + "nvram_raid_rebuild_progress", + "nvram_raid_rebuild_progress_fraction", + "nvram_raid_state", + "online_start_time", + "perf_check", + "physical_capacity", + "physical_drr_percent", + "physical_space", + "physical_space_in_use", + "physical_space_in_use_percent", + "physical_space_in_use_tb", + "physical_space_in_use_wo_overhead", + "physical_space_tb", + "physical_space_wo_overhead", + "provides_blocked", + "quotas_allocated_capacity", + "quotas_used_capacity", + "quotas_used_percent", + "raid_drives_can_fail", + "raid_rebuild_progress", + "rd_bw", + "rd_bw_mb", + "rd_iops", + "rd_latency", + "rd_latency_ms", + "rd_md_iops", + "read_bw", + "read_iops", + "remaining_stripes_health", + "replication_bw_mb", + "replication_iops", + "replication_latency_ms", + "replication_rd_bw_mb", + "replication_rd_iops", + "replication_rd_latency_ms", + "replication_wr_bw_mb", + "replication_wr_iops", + "replication_wr_latency_ms", + "rewrite_phase", + "rewrite_progress", + "rewrite_status", + "rewrite_type", + "rio_nvram_state", + "rio_raid_rebuild_progress", + "rio_raid_rebuild_progress_fraction", + "s3_new_version", + "ssd_capacity", + "ssd_raid_rebuild_progress", + "ssd_raid_state", + "ssh_user", + "state", + "sw_version", + "system_name", + "system_settings", + "title", + "triplication_enabled", + "turbo_boost_flag", + "upgrade_phase", + "upgrade_progress", + "upgrade_state", + "uptime", + "url", + "usable_auxiliary_space_in_use", + "usable_capacity", + "usable_capacity_bytes", + "usable_capacity_tb", + "usable_logical_capacity", + "usable_physical_capacity", + "usable_ssd_capacity", + "used_handles_count", + "used_handles_percent", + "vast_audit_log_state", + "wr_bw", + "wr_bw_mb", + "wr_iops", + "wr_latency", + "wr_latency_ms", + "wr_md_iops", + "write_bw", + "write_iops", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "viewpolicies": { + "read_only_fields": { + "change", + "cluster", + "count_views", + "created", + "data_create_delete", + "data_modify", + "data_read", + "enable_listing_of_snapshot_dir", + "enable_snapshot_lookup", + "full", + "guid", + "id", + "internal", + "log_deleted", + "log_full_path", + "log_hostname", + "log_username", + "pretty_atime_frequency", + "pretty_auth_source", + "read", + "remote_mapping", + "s3_bucket_listing", + "s3_bucket_read", + "s3_bucket_read_acp", + "s3_bucket_write", + "s3_bucket_write_acp", + "s3_object_full_control", + "s3_object_read", + "s3_object_read_acp", + "s3_object_write", + "s3_object_write_acp", + "smb_directory_mode_padded", + "smb_file_mode_padded", + "sync", + "sync_time", + "tenant_name", + "title", + "url", + "views_count", + }, + "immutable_fields": {"tenant_id"}, + "set_like_lists": {"protocols_audit", "trash_access"}, + "lookup_field": "name", + }, + "tenants": { + "read_only_fields": { + "ad_title", + "client_ip_ranges_summary", + "created", + "data_engine_enabled", + "dir", + "encryption_group_id", + "encryption_group_state", + "encryption_status", + "encryption_transition_state", + "guid", + "id", + "krb_provider_title", + "ldap_title", + "local_provider", + "local_provider_title", + "logical_capacity", + "nis_title", + "oidc_provider_title", + "physical_capacity", + "quotas_count", + "smb_allowed", + "sync", + "sync_time", + "title", + "url", + "views_count", + "vippool_names", + "vippools", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "quotas": { + "read_only_fields": { + "cluster", + "cluster_id", + "effective_quota_capacity_tb", + "guid", + "id", + "internal", + "last_user_quotas_update", + "num_blocked_users", + "num_exceeded_users", + "percent_capacity", + "percent_inodes", + "pretty_grace_period", + "pretty_grace_period_expiration", + "pretty_state", + "state", + "sync_state", + "system_id", + "tenant_name", + "time_to_block", + "title", + "url", + "used_capacity", + "used_capacity_tb", + "used_effective_capacity", + "used_effective_capacity_tb", + "used_inodes", + "used_limited_capacity", + }, + "immutable_fields": {"path"}, + "set_like_lists": set(), + "lookup_field": "path", + "field_normalizers": { + "user_quotas": normalize_list_by_user_schema, + "group_quotas": normalize_list_by_user_schema, + "default_user_quota": normalize_dict_by_user_schema, + "default_group_quota": normalize_dict_by_user_schema, + }, + }, + "vippools": { + "read_only_fields": { + "active_cnode_ids", + "active_connections", + "active_interfaces", + "bgp_config_guid", + "bgp_config_name", + "cluster", + "cnodes", + "created", + "guid", + "id", + "ranges_summary", + "state", + "sync", + "sync_time", + "tenant_name", + "title", + "url", + }, + "immutable_fields": {"role"}, + "set_like_lists": {"cnode_ids", "vlan_ids"}, + "lookup_field": "name", + }, + "protectionpolicies": { + "read_only_fields": { + "created", + "guid", + "handle", + "id", + "internal", + "is_local", + "is_on_schedule", + "native_replication_remote_target", + "pretty_schedules", + "protected_paths_count", + "remote_tenant", + "replication_target", + "schedule_miss", + "state", + "streams_count", + "sync_interval", + "target_guid", + "target_name", + "tenant", + "title", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + "field_normalizers": { + "frames": normalize_frames, + }, + }, + "protectedpaths": { + "read_only_fields": { + "aggr_phys_estimation", + "bucket_name", + "bw", + "created", + "estimated_read_only_time", + "eta", + "failback_allowed", + "failure_reason", + "guid", + "health", + "id", + "inode_count", + "internal", + "is_gn_enabled", + "is_local", + "last_restore_point_creation_time", + "last_restore_point_time", + "last_run_state", + "last_snapshot_creation_time", + "last_uploading_restore_point_logical_size", + "last_uploading_restore_point_physical_size", + "last_uploading_restore_point_progress", + "last_uploading_restore_point_state", + "logical_size", + "members_info", + "peer_cluster_name", + "peer_connection_state", + "physical_size", + "progress", + "protection_policy_name", + "remote_target_path", + "remote_tenant_name", + "replication_policy", + "replication_stream_roles", + "replication_streams", + "replication_target_name", + "restore_progress", + "restore_task", + "role", + "role_change_eta_sec", + "role_change_progress_promil", + "state_description", + "tenant_name", + "url", + }, + "immutable_fields": {"protection_policy_id", "source_dir", "target_exported_dir"}, + "set_like_lists": set(), + "lookup_field": "name", + }, + "snapshots": { + "read_only_fields": { + "aggr_phys_estimation", + "cluster", + "created", + "data_create_time", + "eta_sec", + "guid", + "id", + "logical_capacity", + "physical_capacity", + "policy", + "protection_policy", + "protection_policy_id", + "state", + "subsystem_related", + "tenant_name", + "title", + "type", + "unique_phys_estimation", + "url", + }, + "immutable_fields": {"path"}, + "set_like_lists": set(), + "lookup_field": "name", + }, + "s3policies": { + "read_only_fields": { + "groups", + "guid", + "id", + "is_replicated", + "tenant_name", + "title", + "url", + "users", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "activedirectory": { + "read_only_fields": { + "created", + "enabled", + "guid", + "id", + "last_ma_pwd_renewal_status", + "ldap", + "ldap_id", + "ldap_urls", + "ma_pwd_change_frequency", + "ma_pwd_update_time", + "name", + "preferred_dc_list", + "scheduled_ma_pwd_change_enabled", + "state", + "tenant_id", + "title", + "url", + }, + "immutable_fields": { + # LDAP-delegated fields (needed for create, stored in linked LDAP config, can't update) + "binddn", + "bindpw", + "method", + "port", + "use_tls", + "use_ldaps", + "use_auto_discovery", + "searchbase", + "group_searchbase", + "urls", + }, + "set_like_lists": set(), + "ephemeral_fields": { + "admin_passwd", # Credentials never returned by API + "bindpw", # LDAP bind password never returned (also in immutable) + }, + "lookup_field": "machine_account_name", + }, + "ldaps": { + "read_only_fields": { + "active_directory", + "active_directory_id", + "guid", + "id", + "posix_primary_provider", + "state", + "tenant_id", + "title", + "url", + }, + "immutable_fields": set(), + "set_like_lists": {"urls"}, + "ephemeral_fields": { + "bindpw", # LDAP bind password never returned by API + }, + "lookup_field": "name", + }, + "dns": { + "read_only_fields": { + "cnodes", + "guid", + "id", + "sync", + "sync_time", + "title", + "url", + }, + "immutable_fields": set(), + "set_like_lists": {"domain_suffixes", "vip_pools"}, + "lookup_field": "name", + }, + "qospolicies": { + "read_only_fields": { + "created", + "guid", + "id", + "io_size_bytes", + "tenant_name", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "groups": { + "read_only_fields": { + "guid", + "id", + "local_provider", + "provider_name", + "s3_policies", + "title", + "url", + }, + "immutable_fields": { + "local_provider_id", + }, # local_provider_id is write-once at creation + "set_like_lists": set(), + "unique_constraints": {"gid", "local_provider_id"}, # Groups are uniquely identified by (gid, local_provider_id) + "lookup_field": "name", + }, + "cnodes": { + "read_only_fields": { + "bios_version", + "bmc_fw_version", + "bmc_state", + "bmc_state_reason", + "box_vendor", + "build", + "cbox", + "cbox_id", + "cluster", + "cpld", + "created", + "data_rdma_port", + "data_tcp_port", + "display_name", + "display_state", + "enabled_ssd_count", + "guid", + "host_label", + "host_opensm_master", + "hostname", + "id", + "ip1", + "ip2", + "ipmi_ip", + "ipv6", + "is_leader", + "is_mgmt", + "is_vms", + "led_status", + "mgmt_ip", + "name", + "new_name", + "opensm_state", + "os_version", + "platform_generation", + "platform_rdma_port", + "platform_tcp_port", + "platform_type", + "position", + "psnt", + "rpm", + "sn", + "ssd_count", + "state", + "sync", + "sync_time", + "title", + "tpm_boot_dev_encryption_status", + "turbo_boost", + "url", + "vlan", + "vms_preferred", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "roles": { + "read_only_fields": { + "created", + "guid", + "id", + "is_admin", + "is_default", + "is_system", + "managers", + "tenant", + "tenant_names", + "tenants", + "url", + }, + "immutable_fields": set(), + "set_like_lists": {"permissions"}, + "lookup_field": "name", + }, + "certificates": { + "read_only_fields": { + "cn", + "created", + "expiry", + "fingerprint", + "guid", + "id", + "issuer", + "state", + "url", + "valid_from", + "valid_to", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "vtasks": { + "read_only_fields": { + "created", + "end_time", + "execution_time", + "guid", + "id", + "info", + "messages", + "name", + "start_time", + "timeout_in_seconds", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "alarms": { + "read_only_fields": { + "alarm_message", + "cluster", + "event", + "event_definition", + "event_name", + "event_type", + "id", + "last_updated", + "metadata", + "object_guid", + "object_id", + "object_name", + "object_type", + "rel_obj_class", + "rel_obj_id", + "severity", + "timestamp", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "id", + }, + "apitokens": { + "read_only_fields": { + "created", + "id", + "last_used", + "revocation_time", + "revoked", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "bgpconfigs": { + "read_only_fields": set(), + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "bigcatalogconfig": { + "read_only_fields": { + "clone_type", + "created", + "guid", + "handle", + "id", + "indestructible", + "internal", + "is_local", + "is_on_schedule", + "name", + "native_replication_remote_target", + "prefix", + "pretty_schedules", + "remote_tenant", + "replication_target", + "schedule_miss", + "state", + "sync_interval", + "target_guid", + "target_name", + "target_object_id", + "tenant", + "tenant_id", + "title", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "blockhosts": { + "read_only_fields": { + "id", + "mapped_volume_count", + "mapped_volumes_preview", + "tenant_name", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "callhomeconfigs": { + "read_only_fields": { + "callhome_upload_destination", + "cloud_registered", + "id", + "ssl_certificate", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "id", + }, + "carriers": { + "read_only_fields": { + "carrier_index", + "carrier_type", + "cluster", + "cluster_id", + "dbox", + "dbox_id", + "display_name", + "fw_version", + "guid", + "hw_version", + "id", + "insertion_time", + "led_status", + "model", + "name", + "nvrams", + "position", + "shelf", + "sn", + "ssds", + "state", + "sw_version", + "title", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "cboxes": { + "read_only_fields": { + "cluster", + "cluster_id", + "guid", + "id", + "index_in_rack", + "name", + "rack_id", + "rack_name", + "state", + "subsystem", + "title", + "uid", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "cnodegroups": { + "read_only_fields": { + "cnodes", + "guid", + "id", + "state", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "config": { + "read_only_fields": { + "is_modified", + "key", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "id", + }, + "dboxes": { + "read_only_fields": { + "arch_type", + "box_vendor", + "cluster", + "cluster_id", + "drive_type", + "dtray", + "guid", + "hardware_type", + "id", + "index_in_rack", + "is_conclude_possible", + "is_migrate_source", + "is_migrate_target", + "is_replace_possible", + "rack_id", + "rack_name", + "state", + "subsystem", + "sync", + "sync_time", + "title", + "uid", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "dnodes": { + "read_only_fields": { + "arch_type", + "bios_version", + "bmc_fw_version", + "bmc_state", + "bmc_state_reason", + "box_rdma_port", + "build", + "cluster", + "cluster_id", + "cpld", + "data_rdma_port", + "data_tcp_port", + "dbox", + "dbox_id", + "display_name", + "dtray", + "ebox", + "ebox_id", + "guid", + "host_label", + "hostname", + "id", + "ip", + "ip1", + "ip2", + "ipmi_ip", + "ipv6", + "is_primary", + "led_status", + "mgmt_ip", + "name", + "new_name", + "os_version", + "platform_rdma_port", + "platform_tcp_port", + "position", + "rpm", + "sn", + "state", + "sync", + "sync_time", + "title", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "dtrays": { + "read_only_fields": { + "bmc_fw_version", + "bmc_ip", + "bmc_state", + "bmc_state_reason", + "cluster", + "cpld_version", + "dbox", + "dbox_id", + "dnodes", + "guid", + "id", + "led_status", + "mcu_state", + "mcu_version", + "name", + "pcie_switch_firmware_version", + "pcie_switch_mfg_version", + "position", + "serial_number", + "state", + "sync", + "title", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "eboxes": { + "read_only_fields": { + "arch_type", + "box_vendor", + "cluster", + "cluster_id", + "description", + "drive_type", + "dtray", + "guid", + "id", + "index_in_rack", + "is_conclude_possible", + "is_replace_possible", + "led_status", + "name", + "rack_id", + "rack_name", + "sn", + "state", + "subsystem", + "sync", + "sync_time", + "title", + "uid", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "encryptedpaths": { + "read_only_fields": { + "encryption_group", + "id", + "tenant_name", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "path", + }, + "encryptiongroups": { + "read_only_fields": { + "crn", + "guid", + "id", + "state", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "id", + }, + "eventdefinitionconfigs": { + "read_only_fields": { + "id", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "id", + }, + "eventdefinitions": { + "read_only_fields": { + "action_definitions", + "alarm_definitions", + "event_message", + "event_type", + "id", + "metadata", + "name", + "object_type", + "property", + "user_modified", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "events": { + "read_only_fields": { + "cluster", + "event_definition", + "event_name", + "event_origin", + "guid", + "id", + "metadata", + "object_guid", + "object_name", + "severity", + "timestamp", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "object_type", + }, + "folders": { + "read_only_fields": set(), + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "id", + }, + "globalsnapstreams": { + "read_only_fields": { + "bw", + "direction", + "eta", + "external_state", + "guid", + "health", + "id", + "loanee_tenant", + "restore_task", + "source_cluster", + "source_path", + "source_snapshot", + "state", + "sync_progress", + "target_cluster", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "iamroles": { + "read_only_fields": { + "guid", + "id", + "tenant", + "vid", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "indestructibility": { + "read_only_fields": { + "guid", + "id", + "is_locked", + "name", + "passwd_delay_eta", + "title", + "token", + "token_time", + "unlock_system_time", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "injections": { + "read_only_fields": { + "values", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "kafkabrokers": { + "read_only_fields": { + "id", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "kerberos": { + "read_only_fields": { + "guid", + "id", + "state", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "realm", + }, + "licenses": { + "read_only_fields": { + "accelerated_cpu_cores", + "capacity", + "cpu_cores", + "expiration_date", + "guid", + "id", + "name", + "period", + "start_date", + "state", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "localproviders": { + "read_only_fields": { + "assigned_tenants_preview", + "id", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "managedapplications": { + "read_only_fields": { + "guid", + "id", + "state", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "managers": { + "read_only_fields": { + "failed_logins", + "full_name", + "guid", + "id", + "is_active", + "is_default", + "last_login", + "object_permissions", + "password_expiration", + "password_retype", + "tenant", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "username", + }, + "monitors": { + "read_only_fields": { + "exclude_patterns", + "id", + "limit", + "metrics_exposure", + "monitor_type", + "query_aggregation", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "nativereplicationremotetargets": { + "read_only_fields": { + "address_count", + "created", + "guid", + "health", + "is_local", + "last_heart_beat", + "peer_name", + "pool", + "pool_name", + "remote_version", + "remote_vip_range", + "secret", + "space_left", + "state", + "state_description", + "status", + "sync_state", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "nicports": { + "read_only_fields": { + "address", + "cluster", + "cluster_id", + "display_name", + "external", + "failure_reason", + "fw_version", + "guid", + "host_id", + "id", + "interface", + "model", + "name", + "port_membership", + "sn", + "state", + "title", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "nis": { + "read_only_fields": { + "guid", + "id", + "posix_primary_provider", + "state", + "tenant_id", + "title", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "nvrams": { + "read_only_fields": { + "arch_type", + "attached_dnode_names", + "carrier", + "carrier_hw_version", + "carrier_serial", + "carrier_sw_version", + "cluster", + "cluster_id", + "dbox", + "dbox_id", + "display_name", + "dnode1_attached", + "dnode2_attached", + "fail_reason", + "fw_version", + "guid", + "id", + "insertion_time", + "is_remote", + "led_status", + "model", + "name", + "phase_out", + "shelf", + "size", + "slot", + "sn", + "space_in_use", + "state", + "title", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "oidcs": { + "read_only_fields": { + "assigned_tenants_preview", + "guid", + "id", + "last_keys_refresh_time", + "state", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "projections": { + "read_only_fields": { + "num_rows", + "properties", + "size", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "racks": { + "read_only_fields": { + "available_capacity", + "cnode_ip_pool", + "cnode_ipmi_pool", + "dnode_ip_pool", + "dnode_ipmi_pool", + "guid", + "id", + "is_bgp_enabled", + "total_capacity", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "realms": { + "read_only_fields": { + "guid", + "id", + "tenant", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "replicationpolicies": { + "read_only_fields": { + "guid", + "id", + "replication_target_name", + "vip_pool", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "replicationstreams": { + "read_only_fields": { + "bw", + "capabilities", + "guid", + "id", + "internal", + "priority_number", + "protected_path_guid", + "protection_policy", + "remote_target_id", + "remote_target_name", + "remote_tenant_name", + "replication_group_id", + "replication_policy", + "role", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "replicationtargets": { + "read_only_fields": { + "created", + "decoded_access_key", + "guid", + "id", + "state", + "state_description", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "s3keys": { + "read_only_fields": set(), + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "id", + }, + "s3lifecyclerules": { + "read_only_fields": { + "guid", + "id", + "title", + "url", + "view_path", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "schemas": { + "read_only_fields": { + "properties", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "snapshotpolicies": { + "read_only_fields": { + "cluster", + "guid", + "humanize_schedule", + "id", + "last_operation_state", + "title", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "path", + }, + "ssds": { + "read_only_fields": { + "arch_type", + "attached_dnode_names", + "carrier", + "carrier_hw_version", + "carrier_serial", + "carrier_sw_version", + "cluster", + "cluster_id", + "dbox", + "dbox_id", + "display_name", + "dnode1_attached", + "dnode2_attached", + "fail_reason", + "fw_version", + "guid", + "id", + "index_in_carrier", + "insertion_time", + "led_status", + "model", + "name", + "shelf", + "size", + "slot", + "sn", + "space_in_use", + "state", + "title", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "subnetmanager": { + "read_only_fields": { + "failure_reason", + "guid", + "id", + "name", + "state", + "title", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "supportbundles": { + "read_only_fields": { + "bundle_file", + "bundle_size", + "bundle_url", + "callhome", + "cluster", + "cluster_id", + "core", + "create_datetime", + "created", + "guid", + "id", + "management", + "mem_traces", + "metrics", + "name", + "performance", + "platform", + "state", + "traces", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "path", + }, + "supporteddrives": { + "read_only_fields": { + "drive_type", + "id", + "model_name", + "raw_data", + "synced", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "switches": { + "read_only_fields": { + "cluster", + "cluster_id", + "configuration_file", + "configured", + "display_name", + "fw_version", + "guid", + "hostname", + "id", + "install", + "ipv6", + "mgmt_gateway", + "mgmt_ip", + "mgmt_subnet", + "model", + "mtu", + "name", + "pair_id", + "peer_switch", + "role", + "sn", + "state", + "switch_id", + "switch_type", + "title", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "tables": { + "read_only_fields": { + "acummulative_row_insertion_count", + "num_rows", + "properties", + "size", + "sorting_done", + "sorting_key_enabled", + "sorting_label", + "sorting_score", + "write_amplification", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "topics": { + "read_only_fields": set(), + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "userquotas": { + "read_only_fields": { + "entity_identifier", + "guid", + "id", + "path", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "vastdb": { + "read_only_fields": set(), + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "id", + }, + "vms": { + "read_only_fields": { + "auto_logout_timeout", + "build", + "capacity_usable", + "created", + "degraded_reason", + "disable_mgmt_ha", + "guid", + "id", + "ip", + "ip1", + "ip2", + "ipv6_support", + "max_api_tokens_per_user", + "mgmt_cnode", + "mgmt_inner_vip_cnode", + "mgmt_ip", + "mgmt_vip_ipv6", + "min_qos_supported", + "name", + "ssl_certificate", + "ssl_keyfile", + "ssl_port", + "state", + "sw_version", + "tabular_support", + "title", + "total_active_capacity", + "total_remaining_capacity", + "total_usage_capacity_percentage", + "url", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "volumes": { + "read_only_fields": { + "capacity", + "created", + "id", + "mapped_block_host_count", + "mapped_block_hosts_preview", + "namespace_id", + "nguid", + "qos_policy", + "snapshot_data", + "tenant_name", + "uuid", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, + "vpntunnels": { + "read_only_fields": { + "cnode_id", + "cnode_ip", + "created_at", + "hostname", + "id", + "tunnel_end_date", + "tunnels_ips", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "username", + }, + "webhooks": { + "read_only_fields": { + "certificate_name", + "id", + }, + "immutable_fields": set(), + "set_like_lists": set(), + "lookup_field": "name", + }, +} + + +def get_overrides(resource: str) -> Dict[str, Any]: + """Get overrides for a specific resource. + + All resources should have entries populated by the generator. + The empty fallback is a safety net, not a data source. + """ + return OVERRIDES.get( + resource, + { + "read_only_fields": set(), + "immutable_fields": set(), + "set_like_lists": set(), + "unique_constraints": set(), + "lookup_field": "name", + }, + ) + + +def get_read_only_fields(resource: str) -> Set[str]: + """Get read-only fields for a resource.""" + return get_overrides(resource).get("read_only_fields", set()) + + +def get_immutable_fields(resource: str) -> Set[str]: + """Get immutable fields for a resource.""" + return get_overrides(resource).get("immutable_fields", set()) + + +def get_set_like_lists(resource: str) -> Set[str]: + """Get set-like list fields for a resource.""" + return get_overrides(resource).get("set_like_lists", set()) + + +def get_ephemeral_fields(resource: str) -> Set[str]: + """Get ephemeral fields for a resource (credentials/secrets never returned by API).""" + return get_overrides(resource).get("ephemeral_fields", set()) + + +def get_lookup_field(resource: str) -> str: + """Get lookup field for a resource.""" + return get_overrides(resource).get("lookup_field", "name") + + +def is_read_only(resource: str, field: str) -> bool: + """Check if a field is read-only for a resource.""" + return field in get_read_only_fields(resource) + + +def is_immutable(resource: str, field: str) -> bool: + """Check if a field is immutable for a resource.""" + return field in get_immutable_fields(resource) + + +def is_set_like(resource: str, field: str) -> bool: + """Check if a field is a set-like list for a resource.""" + return field in get_set_like_lists(resource) + + +def is_ephemeral(resource: str, field: str) -> bool: + """Check if a field is ephemeral (credentials/secrets) for a resource.""" + return field in get_ephemeral_fields(resource) + + +def get_unique_constraints(resource: str) -> Set[str]: + """Get unique constraint fields for a resource.""" + return get_overrides(resource).get("unique_constraints", set()) + + +def is_unique_constraint(resource: str, field: str) -> bool: + """Check if a field is part of unique constraints.""" + return field in get_unique_constraints(resource) diff --git a/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/sub_endpoint_resource.py b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/sub_endpoint_resource.py new file mode 100644 index 000000000..abdfff82b --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/sub_endpoint_resource.py @@ -0,0 +1,306 @@ +"""Base class for sub-endpoint Ansible modules. + +Provides common runtime logic for all sub-endpoint modules, eliminating +per-module boilerplate. Sub-endpoint modules become thin class declarations: + + class ManagerPassword(SubEndpointResource): + parent_resource = "managers" + sub_path = "password" + path_has_id = False + supported_operations = frozenset({CrudCapability.UPDATE}) +""" + +from enum import Enum +from typing import Any, Dict, FrozenSet, List, Optional, Set + +from ansible.module_utils.basic import AnsibleModule + +from .auth import build_connection, validate_auth +from .client import VastClient +from .errors import VastAPIError, VastError +from .version import ensure_supported_version + + +class CrudCapability(str, Enum): + """Supported CRUD operations for a resource or sub-endpoint.""" + + CREATE = "create" # POST + READ = "read" # GET + UPDATE = "update" # PATCH / PUT + DELETE = "delete" # DELETE + + +class SubEndpointResource: + """Base class for sub-endpoint Ansible modules. + + Subclasses must define: + - parent_resource: str # e.g. "managers" + - sub_path: str # e.g. "password" + - path_has_id: bool # True if /parent/{id}/sub_path + - supported_operations: FrozenSet[CrudCapability] + + Subclasses may override: + - parent_id_param: str # default: singular(parent_resource) + "_id" + - is_async: bool # default: False + - related_sub_paths: list # for action dispatch (e.g. encryption_group_control) + - get(), create(), update(), delete() for custom behavior + """ + + parent_resource: str = NotImplemented + sub_path: str = NotImplemented + path_has_id: bool = True + supported_operations: FrozenSet[CrudCapability] = frozenset() + + parent_id_param: str = "" + is_async: bool = False + related_sub_paths: List[str] = [] + identity_params: List[str] = [] + + # Framework params excluded from payloads + _EXCLUDE_KEYS: Set[str] = {"vms", "state", "wait", "wait_timeout"} + + def __init__(self, module: AnsibleModule): + self.module = module + self.params = module.params + self.check_mode = module.check_mode + + try: + validate_auth(self.params) + except Exception as e: + module.fail_json(msg=str(e)) + + conn = build_connection(self.params) + try: + self.client = VastClient(conn) + except RuntimeError as e: + module.fail_json(msg=str(e)) + + ensure_supported_version(module, self.client, min_version=(5, 4, 0), max_version=(5, 5, 0)) + + # Build the set of keys to exclude from payloads + self._exclude = set(self._EXCLUDE_KEYS) + if self.path_has_id and self.parent_id_param: + self._exclude.add(self.parent_id_param) + if self.related_sub_paths: + self._exclude.add("action") + + @property + def _api_base(self) -> Any: + """Build API accessor for this sub-endpoint.""" + api = self.client.api[self.parent_resource] + if self.path_has_id: + parent_id = self.params[self.parent_id_param] + return api[parent_id][self.sub_path] + return api[self.sub_path] + + def _api_for_action(self, action: str) -> Any: + """Build API accessor for a specific action (for multi-action sub-endpoints).""" + api = self.client.api[self.parent_resource] + if self.path_has_id: + parent_id = self.params[self.parent_id_param] + return api[parent_id][action] + return api[action] + + def _build_payload(self) -> Dict[str, Any]: + """Build payload from module params, excluding framework params.""" + return {key: value for key, value in self.params.items() if key not in self._exclude and value is not None} + + def _build_search_params(self) -> Dict[str, Any]: + """Build search params for collection-level GET (no parent ID).""" + return self._build_payload() + + def get(self) -> Optional[Dict[str, Any]]: + """Read current state of the sub-endpoint.""" + try: + if self.path_has_id: + return self._api_base.first() or {} + else: + search = self._build_search_params() + return self._api_base.first(**search) or {} + except Exception as e: + self.module.fail_json(msg=f"Failed to read {self.sub_path}: {str(e)}") + + def create(self, payload: Dict[str, Any]) -> Dict[str, Any]: + """Create via POST.""" + try: + result = self._api_base.post(**payload) + except Exception as e: + raise VastAPIError(f"Failed to create {self.sub_path}: {e}") from e + if self.is_async: + self._wait_for_task(result) + return result or {} + + def update(self, payload: Dict[str, Any]) -> Dict[str, Any]: + """Update via PATCH.""" + try: + result = self._api_base.patch(**payload) + except Exception as e: + raise VastAPIError(f"Failed to update {self.sub_path}: {e}") from e + if self.is_async: + self._wait_for_task(result) + return result or {} + + def delete(self) -> Dict[str, Any]: + """Delete the sub-endpoint resource.""" + try: + result = self._api_base.delete() + except Exception as e: + raise VastAPIError(f"Failed to delete {self.sub_path}: {e}") from e + if self.is_async: + self._wait_for_task(result) + return result or {} + + def trigger_action(self, action: str) -> Dict[str, Any]: + """Trigger a named action (for multi-action sub-endpoints like encryption_group_control).""" + try: + result = self._api_for_action(action).post() + except Exception as e: + raise VastAPIError(f"Failed to {action}: {e}") from e + if self.is_async: + self._wait_for_task(result) + return result or {} + + def run(self) -> None: + """Main execution logic -- dispatches based on supported_operations.""" + caps = self.supported_operations + state = self.params.get("state", "present") + + # Multi-action trigger (e.g. encryption_group_control) + if self.related_sub_paths: + self._run_action_trigger() + return + + # Read-only sub-endpoint + if caps == frozenset({CrudCapability.READ}): + self._run_read_only() + return + + # Read + Update (idempotent sub-resource) + if CrudCapability.READ in caps and CrudCapability.UPDATE in caps: + self._run_read_update(state) + return + + # Action sub-endpoint (POST/PATCH/PUT, possibly with DELETE) + if CrudCapability.DELETE in caps and state == "absent": + self._perform_delete() + return + + self._run_action() + + def _run_read_only(self) -> None: + """Read-only query module.""" + current = self.get() + self.module.exit_json(changed=False, **{self.module_result_key: current or {}}) + + def _run_read_update(self, state: str) -> None: + """Read + update sub-resource (idempotent).""" + current = self.get() + + if state == "absent": + if CrudCapability.DELETE in self.supported_operations and current: + self._perform_delete() + self.module.exit_json(changed=False, **{self.module_result_key: current or {}}) + + payload = self._build_payload() + if not payload: + self.module.exit_json(changed=False, **{self.module_result_key: current or {}}) + + changed = False + if current: + for key, value in payload.items(): + # Skip identity params they're used for querying, not for updates + if key in self.identity_params: + continue + if current.get(key) != value: + changed = True + break + else: + changed = True + + result = current or {} + diff_before = dict(current) if current else {} + diff_after = {} + + if changed and not self.check_mode: + try: + result = self.update(payload) + except VastAPIError as e: + self.module.fail_json(msg=str(e)) + diff_after = dict(result) + elif changed: + diff_after = {**diff_before, **payload} + result = diff_after + + output: Dict[str, Any] = {"changed": changed, self.module_result_key: result} + if changed: + output["diff"] = {"before": diff_before, "after": diff_after} + self.module.exit_json(**output) + + def _run_action(self) -> None: + """Action sub-endpoint (POST/PATCH/PUT with body).""" + payload = self._build_payload() + + if not self.check_mode: + try: + if CrudCapability.CREATE in self.supported_operations: + result = self.create(payload) + elif CrudCapability.UPDATE in self.supported_operations: + result = self.update(payload) + else: + result = self.create(payload) + except VastAPIError as e: + self.module.fail_json(msg=str(e)) + else: + result = payload + + self.module.exit_json(changed=True, result=result) + + def _perform_delete(self) -> None: + """Delete action.""" + if not self.check_mode: + try: + self.delete() + except VastAPIError as e: + self.module.fail_json(msg=str(e)) + self.module.exit_json(changed=True, **{self.module_result_key: {}}) + + def _run_action_trigger(self) -> None: + """Multi-action trigger (e.g. encryption_group_control).""" + action = self.params.get("action", self.sub_path) + + if self.check_mode: + self.module.exit_json(changed=True, result={}) + + try: + result = self.trigger_action(action) + except VastAPIError as e: + self.module.fail_json(msg=str(e)) + + self.module.exit_json(changed=True, result=result) + + @property + def module_result_key(self) -> str: + """Key name for the result in module output. + + Defaults to the module name (derived from class name). + Subclasses can override for custom result keys. + """ + # Convert CamelCase to snake_case + name = type(self).__name__ + import re + + return re.sub(r"(? None: + """Wait for an async task to complete.""" + if not self.params.get("wait", True): + return + if response is None: + return + + task_id = VastClient.extract_task_id(response) + if task_id: + try: + self.client.wait_for_task(task_id, timeout=self.params.get("wait_timeout", 300)) + except VastError as e: + self.module.fail_json(msg=f"Async task {task_id} failed: {str(e)}") diff --git a/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/timeouts.py b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/timeouts.py new file mode 100644 index 000000000..8a2ee9049 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/timeouts.py @@ -0,0 +1,17 @@ +"""Centralized timeout constants for async VAST operations. + +Values sourced from Orion's battle-tested system_consts +(comet/cluster_config.py) to ensure Ansible modules use +realistic timeouts for real-cluster operations. +""" + +# Task polling default (used by BaseResource._wait_for_task) +DEFAULT_TASK_TIMEOUT = 300 + +# CNode operations +CNODE_STATE_CHANGE_TIMEOUT = 600 +CNODE_REPLACEMENT_TIMEOUT = 3600 + +# DNode operations +DNODE_STATE_CHANGE_TIMEOUT = 600 +DNODE_REPLACEMENT_TIMEOUT = 3600 diff --git a/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/version.py b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/version.py new file mode 100644 index 000000000..be36ee30d --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/module_utils/vast/version.py @@ -0,0 +1,137 @@ +"""VAST product version validation for module compatibility.""" + +from typing import Optional, Tuple + +from ansible.module_utils.basic import AnsibleModule + +from .client import VastClient +from .errors import VastAPIError + + +def parse_version(version_str: str) -> Tuple[int, int, int]: + """ + Parse a version string like '5.4.0' or '5.4.0-123' into (major, minor, patch). + + Raises ValueError if the version string is invalid. + """ + try: + # Strip any build metadata or pre-release info (e.g., '5.4.0-123' -> '5.4.0') + base_version = version_str.split("-")[0].split("+")[0] + parts = base_version.split(".") + if len(parts) < 2: + raise ValueError("Version must have at least major.minor") + major = int(parts[0]) + minor = int(parts[1]) + patch = int(parts[2]) if len(parts) >= 3 else 0 + return (major, minor, patch) + except (ValueError, IndexError, AttributeError) as e: + raise ValueError(f"Invalid version string {version_str!r}: {e}") from e + + +def get_product_version(client: VastClient) -> str: + """ + Query the VAST product version from the API. + + Returns version string (e.g., '5.4.0.20.10960402906660116571'). + Raises VastAPIError if the version cannot be retrieved. + """ + try: + clusters = client.api.clusters.get() + if not clusters: + raise VastAPIError("Failed to retrieve cluster information: clusters.get() returned empty or invalid data") + + sw_version = clusters[0].get("sw_version") + if not sw_version: + raise VastAPIError("Failed to retrieve product version: sw_version field not found in cluster data") + + return sw_version + + except VastAPIError: + raise + except Exception as e: + raise VastAPIError(f"Failed to retrieve product version: {e}") from e + + +def is_version_supported( + version_str: str, min_version: Tuple[int, int, int] = (5, 4, 0), max_version: Optional[Tuple[int, int, int]] = (5, 5, 0) +) -> Tuple[bool, str]: + """ + Check if a product version is within the supported range. + + Args: + version_str: Version string to check (e.g., '5.4.0') + min_version: Minimum supported version (inclusive), default (5, 4, 0) + max_version: Maximum supported version (exclusive), default (5, 5, 0) for 5.4.x series. + If None, no upper bound is enforced. + + Returns: + Tuple of (is_supported, reason_message) + """ + try: + version = parse_version(version_str) + except ValueError as e: + return (False, f"Invalid version format: {e}") + + if version < min_version: + min_str = ".".join(map(str, min_version)) + return (False, f"Version {version_str} is below minimum supported version {min_str}") + + if max_version is not None and version >= max_version: + max_str = ".".join(map(str, max_version)) + return ( + False, + f"Version {version_str} is at or above unsupported version {max_str} " + f"(supports up to {max_version[0]}.{max_version[1] - 1}.x)", + ) + + return (True, "") + + +def ensure_supported_version( + module: AnsibleModule, + client: VastClient, + min_version: Tuple[int, int, int] = (5, 4, 0), + max_version: Optional[Tuple[int, int, int]] = (5, 5, 0), +) -> str: + """ + Validate that the target VAST product version is supported. + + This should be called early in module execution (before making changes). + Fails the module with a clear error message if the version is unsupported. + + Args: + module: AnsibleModule instance + client: VastClient instance + min_version: Minimum supported version (inclusive), default (5, 4, 0) + max_version: Maximum supported version (exclusive), default (5, 5, 0). + Set to None to disable upper bound checking. + + Returns: + The detected product version string (if supported). + """ + try: + product_version = get_product_version(client) + except VastAPIError as e: + module.fail_json( + msg="Failed to validate product version compatibility", + details=str(e), + ) + + supported, reason = is_version_supported(product_version, min_version, max_version) + + if not supported: + min_str = ".".join(map(str, min_version)) + if max_version: + max_ver = (max_version[0], max_version[1] - 1) + support_range = f"{min_str[:3]}.x (up to {max_ver[0]}.{max_ver[1]}.x)" + else: + support_range = f"{min_str} and later" + + module.fail_json( + msg="VAST product version not supported", + detected_version=product_version, + supported_versions=support_range, + reason=reason, + ) + + return product_version diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/dns.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/dns.py new file mode 100644 index 000000000..04d71f98d --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/dns.py @@ -0,0 +1,301 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: dns +short_description: Create, update, or delete VAST DNS resources +description: + - Create, update, or delete VAST DNS resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + bgp_config_id: + description: "The ID of the BGP configuration to use to configure layer 3 connectivity" + type: int + + cnode_ids: + description: "To dedicate a specific group of CNodes to the DNS, list the IDs of the CNodes." + type: list + elements: int + + domain_suffix: + description: | + A suffix for domain names. Requests for domain names with this suffix are resolved to the VIPs configured on the + cluster. + type: str + + enable_l3: + description: "Enable layer 3 connectivity" + type: bool + + enabled: + description: "Set to true to enable the DNS service" + type: bool + + invalid_name_response: + description: "Invalid Name Response" + type: str + + invalid_type_response: + description: "Invalid Type Response" + type: str + + name: + description: "Name" + type: str + + net_type: + description: "Net Type" + type: str + + port: + description: "Specifies a port for the DNS" + type: int + + ttl: + description: "Specifies the TTL value for the DNS." + type: int + + vip: + description: | + A virtual IP to assign to the DNS service. DNS requests from your external DNS server must be delegated to this + IP. + type: str + + vip_gateway: + description: | + If the external DNS server doesn't reside on the same subnet as the DNS VIP, enter the IP of a gateway through + which to connect to the DNS server. + type: str + + vip_ipv6: + description: "Assigns an IPv6 to the DNS service." + type: str + + vip_ipv6_gateway: + description: "Specifies a gateway IPv6 to external DNS server if on different subnet." + type: str + + vip_ipv6_subnet_cidr: + description: "Specifies the subnet, as a CIDR index, on which the DNS resides. [1..128]" + type: int + + vip_subnet_cidr: + description: "The subnet, in CIDR format, on which the DNS VIP resides." + type: int + + vip_vlan: + description: | + If your external DNS server is only exposed to a specific VLAN, you can enter the VLAN here to enable + communication with the DNS server. + type: int + state: + description: Desired state of the resource. + type: str + choices: [present, absent] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Create DNS + vastdata.vms.dns: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + vip: "example-vip" + state: present + +- name: Update DNS + vastdata.vms.dns: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: present + +- name: Delete DNS + vastdata.vms.dns: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: absent +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +dns: + description: The dns resource data. + type: dict + returned: always + sample: + id: 1 + name: "example" +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "bgp_config_id": {"type": "int", "default": None}, + "cnode_ids": {"type": "list", "default": None, "elements": "int"}, + "domain_suffix": {"type": "str", "default": None}, + "enable_l3": {"type": "bool", "default": None}, + "enabled": {"type": "bool", "default": None}, + "invalid_name_response": {"type": "str", "default": None}, + "invalid_type_response": {"type": "str", "default": None}, + "name": {"type": "str", "default": None}, + "net_type": {"type": "str", "default": None}, + "port": {"type": "int", "default": None}, + "ttl": {"type": "int", "default": None}, + "vip": {"type": "str", "default": None}, + "vip_gateway": {"type": "str", "default": None}, + "vip_ipv6": {"type": "str", "default": None}, + "vip_ipv6_gateway": {"type": "str", "default": None}, + "vip_ipv6_subnet_cidr": {"type": "int", "default": None}, + "vip_subnet_cidr": {"type": "int", "default": None}, + "vip_vlan": {"type": "int", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class DnsResource(BaseResource): + """Resource manager for DNS.""" + + resource_name = "dns" + singular = "dns" + lookup_field = "name" + create_only_fields = {"enable_l3"} # Fields only valid during creation + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = DnsResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/eventdefinitionconfigs.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/eventdefinitionconfigs.py new file mode 100644 index 000000000..357764fd0 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/eventdefinitionconfigs.py @@ -0,0 +1,354 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: eventdefinitionconfigs +short_description: Update VAST EventDefinitionConfig resources +description: + - Update VAST EventDefinitionConfig resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + audit_logs_retention: + description: "Audit logs retention in days" + type: int + + critical_value: + description: "Maps CRITICAL severity to a different value. Default: CRITICAL" + type: str + choices: ["INFO", "MINOR", "MAJOR", "CRITICAL"] + + disable_actions: + description: "Set to true to disable default actions for events." + type: bool + + email_recipients: + description: | + Default email recipients. These recipients receive notifications of all alarms except those triggered by events + that have a different list of email recipients specified in the event definition or for which actions are + disabled. + type: list + elements: str + + email_sender: + description: "Global for all alarm notification emails, the sender email that appears in the emails." + type: str + + email_subject: + description: | + Optional and global email subject for all alarm notification emails. Leave blank to send alarm info in the + subject. + type: str + + enabled: + description: "Enabled" + type: bool + + info_value: + description: "Maps INFO severity to a different severity value. Default: INFO" + type: str + choices: ["INFO", "MINOR", "MAJOR", "CRITICAL"] + + major_value: + description: "Maps MAJOR severity to a different severity value. Default: MAJOR" + type: str + choices: ["INFO", "MINOR", "MAJOR", "CRITICAL"] + + minor_value: + description: "Maps MINOR severity to a different severity value. Default: MINOR" + type: str + choices: ["INFO", "MINOR", "MAJOR", "CRITICAL"] + + quota_email_hourly_limit: + description: "Maximum quota alert emails VMS will send per hour" + type: int + + quota_email_interval: + description: "The minimal interval time between quota alert emails sent to a user." + type: str + + quota_email_provider: + description: | + Specify which query context should be used to query providers for user quota alert emails. 'Aggregated' will + perform an aggregated query of all providers. Alternatively, you can specify a specific provider if connected to + the cluster. + type: str + choices: ["aggregated", "ldap", "ad"] + + quota_email_suffix: + description: | + A default suffix to add to append to user names to form an email address. This is used as the email recipient + address for sending a user user quota alert emails. It is only used if an email address is not found for the user + on a provider. + type: str + + smtp_host: + description: "SMTP server host name for alert emails." + type: str + + smtp_password: + description: "Password for SMTP authentication" + type: str + + smtp_port: + description: "The port used by the SMTP server to send outgoing emails. " + type: str + + smtp_use_tls: + description: "Set to true to send email over a TLS connection." + type: bool + + smtp_user: + description: "User for SMTP authentication" + type: str + + syslog_host: + description: "The syslog server's IP address, for sending event logs to a syslog server." + type: str + + syslog_ipmi_audit: + description: "CNode and DNode IPMI commands" + type: bool + + syslog_port: + description: "The port number used by the syslog server to listen on for syslog requests." + type: int + + syslog_protocol: + description: "The protocol used for communicating with the remote syslog server." + type: str + choices: ["tcp", "udp"] + + syslog_shell_audit: + description: "CNode and DNode shell commands" + type: bool + + syslog_vms_audit: + description: "VMS audit" + type: bool + state: + description: Desired state of the resource (update-only; absent is not supported). + type: str + choices: [present] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Update EventDefinitionConfig + vastdata.vms.eventdefinitionconfigs: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + id: "example-value" + state: present +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +eventdefinitionconfigs: + description: The eventdefinitionconfigs resource data. + type: dict + returned: always + sample: + id: 1 +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "audit_logs_retention": {"type": "int", "default": None}, + "critical_value": {"type": "str", "default": None, "choices": ["INFO", "MINOR", "MAJOR", "CRITICAL"]}, + "disable_actions": {"type": "bool", "default": None}, + "email_recipients": {"type": "list", "default": None, "elements": "str"}, + "email_sender": {"type": "str", "default": None}, + "email_subject": {"type": "str", "default": None}, + "enabled": {"type": "bool", "default": None}, + "info_value": {"type": "str", "default": None, "choices": ["INFO", "MINOR", "MAJOR", "CRITICAL"]}, + "major_value": {"type": "str", "default": None, "choices": ["INFO", "MINOR", "MAJOR", "CRITICAL"]}, + "minor_value": {"type": "str", "default": None, "choices": ["INFO", "MINOR", "MAJOR", "CRITICAL"]}, + "quota_email_hourly_limit": {"type": "int", "default": None}, + "quota_email_interval": {"type": "str", "default": None}, + "quota_email_provider": {"type": "str", "default": None, "choices": ["aggregated", "ldap", "ad"]}, + "quota_email_suffix": {"type": "str", "default": None}, + "smtp_host": {"type": "str", "default": None}, + "smtp_password": {"type": "str", "default": None, "no_log": True}, + "smtp_port": {"type": "str", "default": None}, + "smtp_use_tls": {"type": "bool", "default": None}, + "smtp_user": {"type": "str", "default": None}, + "syslog_host": {"type": "str", "default": None}, + "syslog_ipmi_audit": {"type": "bool", "default": None}, + "syslog_port": {"type": "int", "default": None}, + "syslog_protocol": {"type": "str", "default": None, "choices": ["tcp", "udp"]}, + "syslog_shell_audit": {"type": "bool", "default": None}, + "syslog_vms_audit": {"type": "bool", "default": None}, + "state": {"type": "str", "choices": ["present"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class EventdefinitionconfigResource(BaseResource): + """Resource manager for EventDefinitionConfig.""" + + resource_name = "eventdefinitionconfigs" + singular = "eventdefinitionconfig" + lookup_field = "id" + update_only_fields = { + "audit_logs_retention", + "critical_value", + "disable_actions", + "email_recipients", + "email_sender", + "email_subject", + "enabled", + "info_value", + "major_value", + "minor_value", + "quota_email_hourly_limit", + "quota_email_interval", + "quota_email_provider", + "quota_email_suffix", + "smtp_host", + "smtp_password", + "smtp_port", + "smtp_use_tls", + "smtp_user", + "syslog_host", + "syslog_ipmi_audit", + "syslog_port", + "syslog_protocol", + "syslog_shell_audit", + "syslog_vms_audit", + } # Fields only valid during updates + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = EventdefinitionconfigResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/globalsnapstreams.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/globalsnapstreams.py new file mode 100644 index 000000000..b134c28db --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/globalsnapstreams.py @@ -0,0 +1,289 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: globalsnapstreams +short_description: Create, update, or delete VAST GlobalSnapStream resources +description: + - Create, update, or delete VAST GlobalSnapStream resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + enabled: + description: | + Enables background sync, in which the snapshot data is copied from the source to the destination after the clone + is created. During the copying stage, read requests are directed to the source if the requested data is not yet + copied. If false, the snapshot data is not copied to the destination. Requests to read data from the cloned + directory continue to read data from the cloned source. + type: bool + + loanee_root_path: + description: "ID of the path you want to clone. Specify only if cloning from a snapshot on the local cluster." + type: str + + loanee_snapshot: + description: "Loanee snapshot name" + type: str + + loanee_snapshot_id: + description: "ID of local snapshot to clone. Specify only if cloning from a snapshot on the local cluster." + type: int + + loanee_tenant_id: + description: "The target tenant for the clone, on the local cluster." + type: int + + name: + description: "Name" + type: str + + owner_root_snapshot: + description: | + Details of the remote snapshot to clone. To retrieve details of snapshots per path, call + /clusters/list_snapshoted_paths_remote/ and clusters/list_clone_snapshoted_paths_remote/. Specify only if cloning + from remote. + type: dict + + owner_tenant: + description: "Owner tenant details" + type: dict + + remote_target: + description: | + The name of a remote replication peer from which to clone a snapshot. Specify only if cloning from remote. + type: str + + remote_target_id: + description: | + The ID of the remote replication peer from which to clone a snapshot. Specify only if cloning from remote. + type: int + + status: + description: "Status" + type: dict + + remove_dir: + description: "Remove Directory (only used when state=absent)" + type: bool + + stop: + description: Perform stop operation. + type: bool + default: false + state: + description: Desired state of the resource. + type: str + choices: [present, absent] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Create GlobalSnapStream + vastdata.vms.globalsnapstreams: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + state: present + +- name: Update GlobalSnapStream + vastdata.vms.globalsnapstreams: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: present + +- name: Delete GlobalSnapStream + vastdata.vms.globalsnapstreams: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: absent +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +globalsnapstreams: + description: The globalsnapstreams resource data. + type: dict + returned: always + sample: + id: 1 + name: "example" +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "enabled": {"type": "bool", "default": None}, + "loanee_root_path": {"type": "str", "default": None}, + "loanee_snapshot": {"type": "str", "default": None}, + "loanee_snapshot_id": {"type": "int", "default": None}, + "loanee_tenant_id": {"type": "int", "default": None}, + "name": {"type": "str", "default": None}, + "owner_root_snapshot": {"type": "dict", "default": None}, + "owner_tenant": {"type": "dict", "default": None}, + "remote_target": {"type": "str", "default": None}, + "remote_target_id": {"type": "int", "default": None}, + "status": {"type": "dict", "default": None}, + "remove_dir": {"type": "bool", "default": None}, + "stop": {"type": "bool", "default": False}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class GlobalsnapstreamResource(BaseResource): + """Resource manager for GlobalSnapStream.""" + + resource_name = "globalsnapstreams" + singular = "globalsnapstream" + lookup_field = "name" + async_create = True + async_delete = True + create_only_fields = { + "loanee_root_path", + "loanee_snapshot", + "loanee_snapshot_id", + "loanee_tenant_id", + "owner_root_snapshot", + "owner_tenant", + "remote_target", + "remote_target_id", + "status", + } # Fields only valid during creation + delete_body_params = {"remove_dir"} # Sent in JSON body on DELETE + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = GlobalsnapstreamResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/groups.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/groups.py new file mode 100644 index 000000000..d07d09a6b --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/groups.py @@ -0,0 +1,229 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: groups +short_description: Create, update, or delete VAST Group resources +description: + - Create, update, or delete VAST Group resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + gid: + description: "group GID" + type: int + + local_provider_id: + description: "Local provider ID" + type: int + + name: + description: "group name" + type: str + + s3_policies_ids: + description: "list of s3 policy ids" + type: list + elements: int + + sid: + description: "group SID" + type: str + state: + description: Desired state of the resource. + type: str + choices: [present, absent] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Create Group + vastdata.vms.groups: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-name" + gid: 1 + local_provider_id: 1 + state: present + +- name: Update Group + vastdata.vms.groups: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: present + +- name: Delete Group + vastdata.vms.groups: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: absent +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +groups: + description: The groups resource data. + type: dict + returned: always + sample: + id: 1 + name: "example" +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "gid": {"type": "int", "default": None}, + "local_provider_id": {"type": "int", "default": None}, + "name": {"type": "str", "default": None}, + "s3_policies_ids": {"type": "list", "default": None, "elements": "int"}, + "sid": {"type": "str", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class GroupResource(BaseResource): + """Resource manager for Group.""" + + resource_name = "groups" + singular = "group" + lookup_field = "name" + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = GroupResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/ldaps.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/ldaps.py new file mode 100644 index 000000000..d40be1e6a --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/ldaps.py @@ -0,0 +1,466 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: ldaps +short_description: Create, update, or delete VAST Ldap resources +description: + - Create, update, or delete VAST Ldap resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + abac_read_only_value_name: + description: "The attribute to use when querying a provider for a read only attribute access check." + type: str + + abac_read_write_value_name: + description: "The attribute to use when querying a provider for a read-write attribute access check." + type: str + + advanced_filter: + description: | + Use this parameter to specify manual filters for the BaseDN. This is useful when accounts are distributed across + OUs and the baseDN needs to be wide to include all accounts, while there are also accounts that you would like to + exclude from user queries. + type: str + + binddn: + description: | + The bind DN for authenticating to the LDAP domain. You can specify any user account that has read access to the + domain. + type: str + + bindpw: + description: | + The password used with the Bind DN to authenticate to the LDAP server. (Note: This credential is not returned by + the API and only used during operations) + type: str + + domain_name: + description: "FQDN of Active Directory domain. Must be resolvable in DNS." + type: str + + domains_with_posix_attributes: + description: | + Allows to enumerate specific domains for POSIX attributes in case posix_attributes_source is set to + SPECIFIC_DOMAINS. + type: list + elements: str + + gid_number: + description: | + Override 'gidNumber' as the attribute of a group entry that contains the group's GID number. When binding VAST + Cluster to AD, you may need to set this to 'gidnumber' (case sensitive). + type: str + + group_login_name: + description: | + The attribute used to query Active Directory for the group login name in NFS ID mapping. Applicable only with + Active Directory and NFSv4. + type: str + + group_searchbase: + description: | + Base DN for group queries within the joined domain only. When auto discovery is enabled, group queries outside + the joined domain use auto-discovered Base DNs. + type: str + + is_vms_auth_provider: + description: | + Enables use of the LDAP for VMS authentication. Two LDAP configurations per cluster can be used for VMS + authentication: one with Active Directory and one without. + type: bool + + mail_property_name: + description: "The attribute to use for the user's email address." + type: str + + match_user: + description: | + The attribute to use when querying a provider for a user that matches a user that was already retrieved from + another provider. A user entry that contains a matching value in this attribute will be considered the same user + as the user previously retrieved. + type: str + + method: + description: "The authentication method configured on the LDAP server for authenticating clients." + type: str + choices: ["anonymous", "simple", "sasl", "krbv4", "krbv41", "krbv42"] + + monitor_action: + description: | + The type of periodic health check that VAST Cluster performs for the LDAP provider. PING (default, less overhead + and impact on the provider) = pings the provider. BIND = binds to the provider. + type: str + choices: ["PING", "BIND"] + + name: + description: "Name of the LDAP configuration" + type: str + + port: + description: "The port of the remote LDAP server. Typical values: 389, 636." + type: int + + posix_account: + description: | + Override 'posixAccount'as the object class that defines a user entry on the LDAP server. When binding VAST + Cluster to AD, set this parameter to 'user' in order for authorization to work properly. + type: str + + posix_attributes_source: + description: "Defines which domains POSIX attributes will be supported from." + type: str + choices: ["JOINED_DOMAIN", "ALL_DOMAINS", "SPECIFIC_DOMAINS", "GC"] + + posix_group: + description: | + Override 'posixGroup' as the object class that defines a group entry on the LDAP server. When binding VAST + Cluster to AD, set this parameter to 'group' in order for authorization to work properly. + type: str + + query_groups_mode: + description: | + A mode setting for how groups are queried: Set to COMPATIBLE to look up user groups using the 'memberOf' and + 'memberUid' attributes. Set to RFC2307BIS_ONLY to look up user groups using only the 'memberOf' attribute. Set to + RFC2307_ONLY to look up user groups using only the 'memberUid' attribute. Set to NONE not to look up user groups + other than by leading GID and primary group SID. + type: str + + query_posix_attributes_from_gc: + description: | + When set to True - users/groups from non-joined domain POSIX attributes are supported, when set to False - Posix + attributes of users/groups from non-joined domain are not supported. As a condition Global catalog needs to be + configured to support Posix attributes. (deprecated since 4.6) + type: bool + + reverse_lookup: + description: "resolve netgroups into hostnames" + type: bool + + searchbase: + description: "The entry in the LDAP directory tree to use as a starting point for user queries." + type: str + + super_admin_groups: + description: "List of groups on the LDAP provider. Members of these groups can log into VMS as cluster admin users." + type: list + elements: str + + tls_certificate: + description: "TLS certificate to use for verifying the remote LDAP server's TLS certificate." + type: str + + uid: + description: | + Override 'uid' as the attribute of a user entry on the LDAP server that contains the user name. When binding VAST + Cluster to AD, you may need to set this to 'sAMAccountname'. + type: str + + uid_member: + description: | + Override 'memberUid' as the attribute of a group entry on the LDAP server that contains names of group members. + When binding VAST Cluster to AD, you may need to set this to 'memberUID' + type: str + + uid_member_value_property_name: + description: "The attribute which represents the value of the LDAP group's member property." + type: str + + uid_number: + description: | + Override 'uidNumber' as the attribute of a user entry on the LDAP server that contains the UID number. Often when + binding VAST Cluster to Active Directory this does not need to be set. + type: str + + urls: + description: | + Comma separated list of URIs of LDAP servers in the format SCHEME://ADDRESS. The order of listing defines the + priority order. The URI with highest priority that has a good health status is used. + type: list + elements: str + + use_auto_discovery: + description: | + When enabled, Active Directory Domain Controllers (DCs) and Active Directory domains are auto discovered. Queries + extend beyond the joined domain to all domains in the forest. When disabled, queries are restricted to the joined + domain and DCs must be provided in the URLs field. + type: bool + + use_ldaps: + description: "Use LDAPS for Auto-Discovery" + type: bool + + use_multi_forest: + description: "Allow access for users from trusted domains on other forests." + type: bool + + use_posix: + description: "POSIX support" + type: bool + + use_tls: + description: "Set to true to enable use of TLS to secure communication between VAST Cluster and the LDAP server." + type: bool + + user_login_name: + description: | + The attribute used to query Active Directory for the user login name in NFS ID mapping. Applicable only with + Active Directory and NFSv4. + type: str + + username_property_name: + description: | + The attribute to use for querying users in VMS user-initated user queries. Default is 'name'. Sometimes set to + 'cn' + type: str + state: + description: Desired state of the resource. + type: str + choices: [present, absent] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Create Ldap + vastdata.vms.ldaps: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + urls: [] + searchbase: "example-searchbase" + state: present + +- name: Update Ldap + vastdata.vms.ldaps: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: present + +- name: Delete Ldap + vastdata.vms.ldaps: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: absent +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +ldaps: + description: The ldaps resource data. + type: dict + returned: always + sample: + id: 1 + name: "example" +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "abac_read_only_value_name": {"type": "str", "default": None}, + "abac_read_write_value_name": {"type": "str", "default": None}, + "advanced_filter": {"type": "str", "default": None}, + "binddn": {"type": "str", "default": None}, + "bindpw": {"type": "str", "default": None, "no_log": True}, + "domain_name": {"type": "str", "default": None}, + "domains_with_posix_attributes": {"type": "list", "default": None, "elements": "str"}, + "gid_number": {"type": "str", "default": None}, + "group_login_name": {"type": "str", "default": None}, + "group_searchbase": {"type": "str", "default": None}, + "is_vms_auth_provider": {"type": "bool", "default": None}, + "mail_property_name": {"type": "str", "default": None}, + "match_user": {"type": "str", "default": None}, + "method": { + "type": "str", + "default": None, + "choices": [ + "anonymous", + "simple", + "sasl", + "krbv4", + "krbv41", + "krbv42", + ], + }, + "monitor_action": {"type": "str", "default": None, "choices": ["PING", "BIND"]}, + "name": {"type": "str", "default": None}, + "port": {"type": "int", "default": None}, + "posix_account": {"type": "str", "default": None}, + "posix_attributes_source": { + "type": "str", + "default": None, + "choices": [ + "JOINED_DOMAIN", + "ALL_DOMAINS", + "SPECIFIC_DOMAINS", + "GC", + ], + }, + "posix_group": {"type": "str", "default": None}, + "query_groups_mode": {"type": "str", "default": None}, + "query_posix_attributes_from_gc": {"type": "bool", "default": None}, + "reverse_lookup": {"type": "bool", "default": None}, + "searchbase": {"type": "str", "default": None}, + "super_admin_groups": {"type": "list", "default": None, "elements": "str"}, + "tls_certificate": {"type": "str", "default": None}, + "uid": {"type": "str", "default": None}, + "uid_member": {"type": "str", "default": None}, + "uid_member_value_property_name": {"type": "str", "default": None}, + "uid_number": {"type": "str", "default": None}, + "urls": {"type": "list", "default": None, "elements": "str"}, + "use_auto_discovery": {"type": "bool", "default": None}, + "use_ldaps": {"type": "bool", "default": None}, + "use_multi_forest": {"type": "bool", "default": None}, + "use_posix": {"type": "bool", "default": None}, + "use_tls": {"type": "bool", "default": None}, + "user_login_name": {"type": "str", "default": None}, + "username_property_name": {"type": "str", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class LdapResource(BaseResource): + """Resource manager for Ldap.""" + + resource_name = "ldaps" + singular = "ldap" + lookup_field = "name" + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = LdapResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/nativereplicationremotetargets.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/nativereplicationremotetargets.py new file mode 100644 index 000000000..425429d9f --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/nativereplicationremotetargets.py @@ -0,0 +1,267 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: nativereplicationremotetargets +short_description: Create, update, or delete VAST NativeReplicationRemoteTarget resources +description: + - Create, update, or delete VAST NativeReplicationRemoteTarget resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + leading_vip: + description: | + Any one of the IP addresses that belong to the remote peer's replication VIP pool. This IP is used for the + initial connection between the peers. + type: str + + mss: + description: "Maximum segment size (MSS), in bytes, that the peer can receive in a single TCP segment." + type: int + + name: + description: "Name of the peer configuration, reflected also on the remote cluster." + type: str + + password: + description: "Not in use" + type: str + + peer_certificate: + description: "Not in use" + type: str + + pool_id: + description: "The ID of the local replication VIP Pool" + type: int + + remote_vips: + description: | + The IP addresses that belong to the remote peer's replication VIP pool. This IP is used for the initial + connection between the peers. Once the connection is established, the peers share their external network topology + and form multiple connections between the VIPs. + type: list + elements: str + + secure_mode: + description: | + Secure mode: NONE=no encryption on the replication connection. SECURE=Replication to this peer will be encrypted + over the wire with mTLS. Requires a certificate, key and root certificate to be uploaded to VMS on each peer + cluster. Upload mTLS certificates with PATCH /clusters/{id}/. + type: str + choices: ["NONE", "SECURE"] + + transport_mode: + description: | + Transport mode: TCP for FIPS compliance, QUIC is not FIPS compliance but good for lower latency in high-latency + networks. + type: str + choices: ["TCP", "QUIC"] + + version: + description: "Version" + type: str + state: + description: Desired state of the resource. + type: str + choices: [present, absent] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Create NativeReplicationRemoteTarget + vastdata.vms.nativereplicationremotetargets: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-name" + leading_vip: "example-leading_vip" + pool_id: 1 + state: present + +- name: Update NativeReplicationRemoteTarget + vastdata.vms.nativereplicationremotetargets: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: present + +- name: Delete NativeReplicationRemoteTarget + vastdata.vms.nativereplicationremotetargets: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: absent +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +nativereplicationremotetargets: + description: The nativereplicationremotetargets resource data. + type: dict + returned: always + sample: + id: 1 + name: "example" +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "leading_vip": {"type": "str", "default": None}, + "mss": {"type": "int", "default": None}, + "name": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "peer_certificate": {"type": "str", "default": None}, + "pool_id": {"type": "int", "default": None}, + "remote_vips": {"type": "list", "default": None, "elements": "str"}, + "secure_mode": {"type": "str", "default": None, "choices": ["NONE", "SECURE"]}, + "transport_mode": {"type": "str", "default": None, "choices": ["TCP", "QUIC"]}, + "version": {"type": "str", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class NativereplicationremotetargetResource(BaseResource): + """Resource manager for NativeReplicationRemoteTarget.""" + + resource_name = "nativereplicationremotetargets" + singular = "nativereplicationremotetarget" + lookup_field = "name" + update_only_fields = {"id", "remote_vips", "version"} # Fields only valid during updates + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = NativereplicationremotetargetResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/nonlocal_group.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/nonlocal_group.py new file mode 100644 index 000000000..61dbe015c --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/nonlocal_group.py @@ -0,0 +1,185 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Sub-endpoint module: groups/query + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: nonlocal_group +short_description: Manage non-local group query data +description: + - Manage non-local group query data. + - Supports check_mode and diff. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + gid: + description: "Group GID" + type: int + + groupname: + description: "Group name" + type: str + + s3_policies_ids: + description: "list of s3 policy ids" + type: list + elements: int + + sid: + description: "Group SID" + type: str + + tenant_id: + description: "Tenant ID" + type: int + + vaid: + description: "Group vast ID" + type: str + state: + description: Desired state. + type: str + choices: [present, absent] + default: present +author: + - VAST Data (@vastdata) +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Manage non-local group query data + vastdata.vms.nonlocal_group: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +nonlocal_group: + description: The sub-resource data. + type: dict + returned: always +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.sub_endpoint_resource import ( + CrudCapability, + SubEndpointResource, +) + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "gid": {"type": "int", "default": None}, + "groupname": {"type": "str", "default": None}, + "s3_policies_ids": {"type": "list", "default": None, "elements": "int"}, + "sid": {"type": "str", "default": None}, + "tenant_id": {"type": "int", "default": None}, + "vaid": {"type": "str", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, +} +# END AUTOGENERATED ARGUMENT_SPEC + + +# BEGIN AUTOGENERATED RESOURCE_CLASS +class NonlocalGroup(SubEndpointResource): + parent_resource = "groups" + sub_path = "query" + path_has_id = False + supported_operations = frozenset({CrudCapability.READ, CrudCapability.UPDATE}) + identity_params = ["tenant_id", "groupname"] + + +# END AUTOGENERATED RESOURCE_CLASS + + +# BEGIN AUTOGENERATED MAIN +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = NonlocalGroup(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/nonlocal_user.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/nonlocal_user.py new file mode 100644 index 000000000..3e9e2832c --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/nonlocal_user.py @@ -0,0 +1,206 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Sub-endpoint module: users/query + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: nonlocal_user +short_description: Manage non-local user query data +description: + - Manage non-local user query data. + - Supports check_mode and diff. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + allow_create_bucket: + description: | + Set to true to give the user permission to create S3 buckets. In case of conflict with an S3 identity policy + attached to the user or to a relevant group, this setting is overridden. + type: bool + + allow_delete_bucket: + description: | + Set to true to give the user permission to delete S3 buckets. In case of conflict with an S3 identity policy + attached to the user or to a relevant group, this setting is overridden. + type: bool + + login_name: + description: "Login name" + type: str + + s3_policies_ids: + description: "Specify S3 policies to attach to the user." + type: list + elements: int + + s3_superuser: + description: | + Set to true for S3 superuser. In case of conflict with an S3 identity policy attached to the user or to a + relevant group, this setting is overridden. + type: bool + + sid: + description: "User SID" + type: str + + tenant_id: + description: "Tenant ID" + type: int + + uid: + description: "User UID" + type: int + + username: + description: "User legal name" + type: str + state: + description: Desired state. + type: str + choices: [present, absent] + default: present +author: + - VAST Data (@vastdata) +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Manage non-local user query data + vastdata.vms.nonlocal_user: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +nonlocal_user: + description: The sub-resource data. + type: dict + returned: always +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.sub_endpoint_resource import ( + CrudCapability, + SubEndpointResource, +) + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "allow_create_bucket": {"type": "bool", "default": None}, + "allow_delete_bucket": {"type": "bool", "default": None}, + "login_name": {"type": "str", "default": None}, + "s3_policies_ids": {"type": "list", "default": None, "elements": "int"}, + "s3_superuser": {"type": "bool", "default": None}, + "sid": {"type": "str", "default": None}, + "tenant_id": {"type": "int", "default": None}, + "uid": {"type": "int", "default": None}, + "username": {"type": "str", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, +} +# END AUTOGENERATED ARGUMENT_SPEC + + +# BEGIN AUTOGENERATED RESOURCE_CLASS +class NonlocalUser(SubEndpointResource): + parent_resource = "users" + sub_path = "query" + path_has_id = False + supported_operations = frozenset({CrudCapability.READ, CrudCapability.UPDATE}) + identity_params = ["tenant_id", "username"] + + +# END AUTOGENERATED RESOURCE_CLASS + + +# BEGIN AUTOGENERATED MAIN +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = NonlocalUser(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/protectedpaths.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/protectedpaths.py new file mode 100644 index 000000000..063ed0884 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/protectedpaths.py @@ -0,0 +1,309 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: protectedpaths +short_description: Create, update, or delete VAST ProtectedPath resources +description: + - Create, update, or delete VAST ProtectedPath resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + abort: + description: "abort the failover process" + type: bool + + capabilities: + description: | + Indicates if the protected path supports global access streams (\"STARED_GLOBAL_NAMESPACE\") or async replication + streams (\"ASYNC_REPLICATION\") or a single stream for synchronous replication (\"SYNC_REPLICATION\") or both + global access and async replication (\"REPLICATION_AND_GN\"). + type: str + + enabled: + description: "enable/pause protected path" + type: bool + + failover: + description: "Trigger failover command" + type: bool + + graceful: + description: "graceful failover" + type: bool + + lease_expiry_time: + description: | + The lease expiry time, in seconds, for a global access protected path. This is the duration for which data that + was already requested at the destination path can be read locally from cache without the destination peer + requesting it from the source peer. When the lease expires, the cache is invalidated and the next read request + for the data is requested again from the source peer. + type: int + + name: + description: "Name" + type: str + + protection_policy_id: + description: "protection policy id" + type: str + + remote_target_id: + description: "Remote target ID for streams with global namespace or synchronous replication capability." + type: int + + remote_tenant_guid: + description: "Tenant GUID of the remote peer tenant to which to replicate" + type: str + + replication_policy_id: + description: "replication policy id" + type: str + + source_dir: + description: "path to replicate" + type: str + + source_member_capabilities: + description: "Stream capabilities for the source member" + type: str + + sync_disconnect_time: + description: | + A period of time, in seconds, without communication between sync replication peers, after which the peers are + disconnected.\" + type: int + + sync_interval: + description: | + Minimal duration since the last snapshot shared between all destination peers in a replication group. Applicable + if the protected path has more than one replication stream. + type: int + + target_exported_dir: + description: "where to replicate on the remote" + type: str + + tenant_id: + description: "Tenant ID of the tenant on the local cluster to which the source_dir belongs." + type: int + state: + description: Desired state of the resource. + type: str + choices: [present, absent] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Create ProtectedPath + vastdata.vms.protectedpaths: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-name" + source_dir: "example-source_dir" + target_exported_dir: "example-target_exported_dir" + tenant_id: 1 + state: present + +- name: Update ProtectedPath + vastdata.vms.protectedpaths: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: present + +- name: Delete ProtectedPath + vastdata.vms.protectedpaths: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: absent +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +protectedpaths: + description: The protectedpaths resource data. + type: dict + returned: always + sample: + id: 1 + name: "example" +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "abort": {"type": "bool", "default": None}, + "capabilities": {"type": "str", "default": None}, + "enabled": {"type": "bool", "default": None}, + "failover": {"type": "bool", "default": None}, + "graceful": {"type": "bool", "default": None}, + "lease_expiry_time": {"type": "int", "default": None}, + "name": {"type": "str", "default": None}, + "protection_policy_id": {"type": "str", "default": None}, + "remote_target_id": {"type": "int", "default": None}, + "remote_tenant_guid": {"type": "str", "default": None}, + "replication_policy_id": {"type": "str", "default": None}, + "source_dir": {"type": "str", "default": None}, + "source_member_capabilities": {"type": "str", "default": None}, + "sync_disconnect_time": {"type": "int", "default": None}, + "sync_interval": {"type": "int", "default": None}, + "target_exported_dir": {"type": "str", "default": None}, + "tenant_id": {"type": "int", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class ProtectedpathResource(BaseResource): + """Resource manager for ProtectedPath.""" + + resource_name = "protectedpaths" + singular = "protectedpath" + lookup_field = "name" + async_delete = True + create_only_fields = { + "capabilities", + "remote_target_id", + "remote_tenant_guid", + "source_member_capabilities", + "tenant_id", + } # Fields only valid during creation + update_only_fields = {"abort", "failover", "graceful", "replication_policy_id", "state"} # Fields only valid during updates + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = ProtectedpathResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/protectionpolicies.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/protectionpolicies.py new file mode 100644 index 000000000..b259cead0 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/protectionpolicies.py @@ -0,0 +1,267 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: protectionpolicies +short_description: Create, update, or delete VAST ProtectionPolicy resources +description: + - Create, update, or delete VAST ProtectionPolicy resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + big_catalog: + description: "Indicates if Protection Policy will be used for VAST Catalog. There may only be 1 such policy." + type: bool + + clone_type: + description: | + Specify the type of data protection. CLOUD_REPLICATION is S3 backup. LOCAL means local snapshots without + replication. + type: str + choices: ["LOCAL", "NATIVE_REPLICATION", "CLOUD_REPLICATION"] + + frames: + description: "Defines the schedule for snapshot creation and the local and remote retention policies." + type: list + elements: dict + + indestructible: + description: | + Set to true to protect the protection policy from accidental or malicious deletion with the indestructibility + feature. If this setting is enabled, authorized unlocking of the cluster's indestructibility mechanism is + required to do any of the following: modifying the policy, deleting the policy or disabling this setting. + type: bool + + name: + description: "Name" + type: str + + prefix: + description: "The prefix for names of snapshots created by the policy" + type: str + + remote_tenant_guid: + description: "remote tenant guid" + type: str + + target_object_id: + description: | + ID of the remote peer. Specify ID of a ReplicationTarget (aka S3 replication peer) if clone_type is + CLOUD_REPLICATION. Specify the ID of a NativeReplicationRemoteTarget if clone_type is NATIVE_REPLICATION. + type: int + + tenant_id: + description: "Tenant ID" + type: int + state: + description: Desired state of the resource. + type: str + choices: [present, absent] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Create ProtectionPolicy + vastdata.vms.protectionpolicies: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-name" + frames: [] + prefix: "example-prefix" + clone_type: "LOCAL" + state: present + +- name: Update ProtectionPolicy + vastdata.vms.protectionpolicies: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: present + +- name: Delete ProtectionPolicy + vastdata.vms.protectionpolicies: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: absent +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +protectionpolicies: + description: The protectionpolicies resource data. + type: dict + returned: always + sample: + id: 1 + name: "example" +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "big_catalog": {"type": "bool", "default": None}, + "clone_type": { + "type": "str", + "default": None, + "choices": [ + "LOCAL", + "NATIVE_REPLICATION", + "CLOUD_REPLICATION", + ], + }, + "frames": {"type": "list", "default": None, "elements": "dict"}, + "indestructible": {"type": "bool", "default": None}, + "name": {"type": "str", "default": None}, + "prefix": {"type": "str", "default": None}, + "remote_tenant_guid": {"type": "str", "default": None}, + "target_object_id": {"type": "int", "default": None}, + "tenant_id": {"type": "int", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class ProtectionpolicyResource(BaseResource): + """Resource manager for ProtectionPolicy.""" + + resource_name = "protectionpolicies" + singular = "protectionpolicy" + lookup_field = "name" + create_only_fields = {"remote_tenant_guid", "tenant_id"} # Fields only valid during creation + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = ProtectionpolicyResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/quotas.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/quotas.py new file mode 100644 index 000000000..1884afbcc --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/quotas.py @@ -0,0 +1,329 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: quotas +short_description: Create, update, or delete VAST Quota resources +description: + - Create, update, or delete VAST Quota resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + create_dir: + description: "Set to true to create the directory if the directory was not created yet." + type: bool + + create_dir_mode: + description: "Unix permissions mode for the new directory" + type: int + + default_email: + description: | + Emails are sent to users if and when they exceed their user/group quota limits. default_email is a default email + address that is used instead of a user's email address in the event that no email address is found for the user + on a provider and no email suffix is set. + type: str + + default_group_quota: + description: | + If is_user_quota is true, this parameter can be used to specify a default rule for all groups. If not specified, + each group is only limited individually by group rules applied to the group through group_quotas. + type: dict + + default_user_quota: + description: | + If is_user_quota is true, this parameter can be used to specify a default rule to limit individual quota usage + for all users. If not specified, each user is only limited individually by user and group rules applied to the + user through user_quotas and group_quotas. + type: dict + + enable_alarms: + description: | + True by default. Enables alarms on relevant events for user and group quotas. Applicable only if is_user_quota is + true. Raises alarms reporting the number of users that exceed their quotas and when one or more users is/are + blocked from writing to the quota directory. + type: bool + + enable_email_providers: + description: | + Set to true to enable querying Active Directory and LDAP services for user emails when sending user notifications + to users if they exceed their user/group quota limits. If enabled, the provider query is the first priority + source for a user's email. If a user's email is not found on the provider, a global suffix is used to form an + email. If no suffix is set, default_email is used. + type: bool + + grace_period: + description: | + Quota enforcement grace period. An alarm is triggered and write operations are blocked if storage usage continues + to exceed the soft limit for the grace period. Format: [DD] [HH:[MM:]]s + type: str + + group_quotas: + description: "Group Quotas" + type: list + elements: dict + + hard_limit: + description: "Storage usage limit beyond which no writes will be allowed." + type: int + + hard_limit_inodes: + description: | + Number of directories and unique files under the path beyond which no writes will be allowed. A file with + multiple hardlinks is counted only once. + type: int + + iam_role_quotas: + description: "Iam Role Quotas" + type: list + elements: dict + + inherit_acl: + description: "Indicates whether the directory should inherit ACLs from its parent directory" + type: bool + + is_user_quota: + description: "Is User Quota" + type: bool + + name: + description: "Quota name" + type: str + + path: + description: "The directory path on which to enforce the quota" + type: str + + soft_limit: + description: "Storage usage limit at which warnings of exceeding the quota are issued." + type: int + + soft_limit_inodes: + description: | + Number of directories and unique files under the path at which warnings of exceeding the quota will be issued. A + file with multiple hardlinks is counted only once. + type: int + + tenant_id: + description: "Tenant ID" + type: int + + user_quotas: + description: | + An array of user quota rule objects. A user quota rule overrides a default user quota rule for the specified + user. + type: list + elements: dict + state: + description: Desired state of the resource. + type: str + choices: [present, absent] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Create Quota + vastdata.vms.quotas: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-name" + path: "example-path" + state: present + +- name: Update Quota + vastdata.vms.quotas: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + path: "example-value" + state: present + +- name: Delete Quota + vastdata.vms.quotas: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + path: "example-value" + state: absent +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +quotas: + description: The quotas resource data. + type: dict + returned: always + sample: + id: 1 + path: "example" +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "create_dir": {"type": "bool", "default": None}, + "create_dir_mode": {"type": "int", "default": None}, + "default_email": {"type": "str", "default": None}, + "default_group_quota": {"type": "dict", "default": None}, + "default_user_quota": {"type": "dict", "default": None}, + "enable_alarms": {"type": "bool", "default": None}, + "enable_email_providers": {"type": "bool", "default": None}, + "grace_period": {"type": "str", "default": None}, + "group_quotas": {"type": "list", "default": None, "elements": "dict"}, + "hard_limit": {"type": "int", "default": None}, + "hard_limit_inodes": {"type": "int", "default": None}, + "iam_role_quotas": {"type": "list", "default": None, "elements": "dict"}, + "inherit_acl": {"type": "bool", "default": None}, + "is_user_quota": {"type": "bool", "default": None}, + "name": {"type": "str", "default": None}, + "path": {"type": "str", "default": None}, + "soft_limit": {"type": "int", "default": None}, + "soft_limit_inodes": {"type": "int", "default": None}, + "tenant_id": {"type": "int", "default": None}, + "user_quotas": {"type": "list", "default": None, "elements": "dict"}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class QuotaResource(BaseResource): + """Resource manager for Quota.""" + + resource_name = "quotas" + singular = "quota" + lookup_field = "path" + create_only_fields = {"create_dir", "create_dir_mode", "inherit_acl", "path", "tenant_id"} # Fields only valid during creation + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = QuotaResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/s3policies.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/s3policies.py new file mode 100644 index 000000000..b3b9e7954 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/s3policies.py @@ -0,0 +1,224 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: s3policies +short_description: Create, update, or delete VAST S3Policy resources +description: + - Create, update, or delete VAST S3Policy resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + enabled: + description: "Enabled if True, Disabled if False" + type: bool + + name: + description: "A new name for the S3 identity policy" + type: str + + policy: + description: "A modified S3 identity policy in JSON format." + type: str + + tenant_id: + description: "Tenant ID" + type: int + state: + description: Desired state of the resource. + type: str + choices: [present, absent] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Create S3Policy + vastdata.vms.s3policies: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-name" + policy: "example-policy" + state: present + +- name: Update S3Policy + vastdata.vms.s3policies: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: present + +- name: Delete S3Policy + vastdata.vms.s3policies: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: absent +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +s3policies: + description: The s3policies resource data. + type: dict + returned: always + sample: + id: 1 + name: "example" +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "enabled": {"type": "bool", "default": None}, + "name": {"type": "str", "default": None}, + "policy": {"type": "str", "default": None}, + "tenant_id": {"type": "int", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class S3policyResource(BaseResource): + """Resource manager for S3Policy.""" + + resource_name = "s3policies" + singular = "s3policy" + lookup_field = "name" + create_only_fields = {"tenant_id"} # Fields only valid during creation + update_only_fields = {"enabled"} # Fields only valid during updates + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = S3policyResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/snapshots.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/snapshots.py new file mode 100644 index 000000000..3fd76313a --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/snapshots.py @@ -0,0 +1,241 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: snapshots +short_description: Create, update, or delete VAST Snapshot resources +description: + - Create, update, or delete VAST Snapshot resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + cluster_id: + description: "Cluster ID" + type: int + + expiration_time: + description: "Snapshot expiration time. Cannot be shortened if the snapshot is indestructible." + type: str + + indestructible: + description: | + Set to true to protect the snapshot from accidental or malicious deletion with the indestructibility feature. If + this setting is enabled, authorized unlocking of the cluster's indestructibility mechanism is required to do any + of the following: deleting the snapshot, shortening its expiration time or disabling this setting. + type: bool + + locked: + description: "Not in use." + type: bool + + name: + description: "Snapshot name" + type: str + + path: + description: "The path to take a snapshot on" + type: str + + tenant_id: + description: "Tenant ID" + type: int + state: + description: Desired state of the resource. + type: str + choices: [present, absent] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Create Snapshot + vastdata.vms.snapshots: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-name" + path: "example-path" + state: present + +- name: Update Snapshot + vastdata.vms.snapshots: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: present + +- name: Delete Snapshot + vastdata.vms.snapshots: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: absent +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +snapshots: + description: The snapshots resource data. + type: dict + returned: always + sample: + id: 1 + name: "example" +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "cluster_id": {"type": "int", "default": None}, + "expiration_time": {"type": "str", "default": None}, + "indestructible": {"type": "bool", "default": None}, + "locked": {"type": "bool", "default": None}, + "name": {"type": "str", "default": None}, + "path": {"type": "str", "default": None}, + "tenant_id": {"type": "int", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class SnapshotResource(BaseResource): + """Resource manager for Snapshot.""" + + resource_name = "snapshots" + singular = "snapshot" + lookup_field = "name" + create_only_fields = {"cluster_id", "path", "tenant_id"} # Fields only valid during creation + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = SnapshotResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/tenants.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/tenants.py new file mode 100644 index 000000000..708d3389c --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/tenants.py @@ -0,0 +1,473 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: tenants +short_description: Create, update, or delete VAST Tenant resources +description: + - Create, update, or delete VAST Tenant resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + access_ip_ranges: + description: | + Restricts tenant login access to specified source IP ranges. Enter as single IPs (1.1.1.1), ranges (1.2.3.4 - + 1.2.3.6), or CIDR (1.1.1.0/24). + type: list + elements: str + + ad_provider_id: + description: "Active Directory provider ID" + type: int + + allow_disabled_users: + description: "Allow IO from users whose Active Directory accounts are explicitly disabled." + type: bool + + allow_locked_users: + description: | + Allow IO from users whose Active Directory accounts are locked out by lockout policies due to unsuccessful login + attempts. + type: bool + + allowed_delegations: + description: "Defines the possible types of NFSv4 delegations" + type: str + choices: ["NONE", "READ", "WRITE", "READ_WRITE"] + + application_users_group_name: + description: "The name of the group that will be used for application users" + type: str + + capacity_rules: + description: "Capacity Rules" + type: dict + + client_ip_ranges: + description: | + Array of ranges of client IPs to be served by the tenant. For client requests made to a VIP that belongs to a VIP + Pool that is not associated to a specific tenant, the client's source IP is checked against the Client IPs that + are defined within each tenant. That check determines access. The client IPs that you associate with each tenant + must be unique per tenant. + type: list + elements: list + + data_engine_role_enabled: + description: "Whether to create dedicated Role for application users group" + type: bool + + data_engine_s3_policy_enabled: + description: "Whether to create dedicated Identity Policy for application users group" + type: bool + + default_others_share_level_perm: + description: "Default Share-level permissions for Others" + type: str + choices: ["READ", "CHANGE", "FULL"] + + domain_name: + description: "Domain name to incorporate into the VMS tenant login page URL." + type: str + + encryption_crn: + description: "Tenant's encryption group unique identifier (deprecated)" + type: str + + encryption_group: + description: "Tenant's encryption group unique identifier" + type: str + + grant_unrequested_delegations_by_default: + description: | + When enabled, the server may grant delegations based on share access even if not explicitly requested by the + client + type: bool + + identity_provider_name: + description: | + Sets a configured SAML login provider to enable for the tenant. When set, users defined on the specified SAML + provider can login to the tenant VMS as tenant admin users. + type: str + + is_nfsv42_supported: + description: "Enable NFSv4.2" + type: bool + + krb_provider_id: + description: "Kerberos provider ID" + type: int + + ldap_provider_id: + description: "Open-LDAP provider ID specified separately by the user" + type: int + + local_provider_id: + description: "Local provider ID" + type: int + + login_name_primary_provider: + description: "Login name primary provider type" + type: str + choices: ["NONE", "LDAP", "NIS", "AD", "LOCAL"] + + max_views: + description: "Max views we can create on this tenant (0:unlimted as default)" + type: int + + name: + description: "A name for the tenant" + type: str + + nis_provider_id: + description: "NIS provider ID" + type: int + + oidc_provider_id: + description: "OIDC provider ID" + type: int + + posix_primary_provider: + description: "POSIX primary provider type" + type: str + choices: ["NONE", "LDAP", "NIS", "AD"] + + preferred_owning_group: + description: "Set to prefer GID of the user as the owning group of the file" + type: str + choices: ["PROTOCOL_BASED", "POSIX_GID"] + + qos: + description: "Qos" + type: dict + + require_smb_signing: + description: "Require SMB signing" + type: bool + + smb_administrators_group_name: + description: | + Optional custom name to specify a non default privileged group. If not set, privileged group is the Backup + Operators domain group. + type: str + + smb_encryption_state: + description: "Defines the encryption level for SMB" + type: str + choices: ["OFF", "AVAILABLE", "DESIRED", "REQUIRED"] + + smb_privileged_group_full_access: + description: | + True=The SMB privileged user group has read and write control access. Members of the group can perform backup and + restore operations on all files and directories, without requiring read or write access to the specific files and + directories. False=the privileged group has read only access. + type: bool + + smb_privileged_group_sid: + description: | + Optional custom SID to specify a non default SMB privileged group. If not set, SMB privileged group is the Backup + Operators domain group. + type: str + + smb_privileged_user_name: + description: | + Optional custom username for the SMB privileged user. If not set, the SMB privileged user name is 'vastadmin' + type: str + + tenant_admins_group_name: + description: | + Specifies a group on an AD or LDAP provider. Enables users in the group to log into the tenant VMS as Tenant + Admin users. In order to be granted permissions to do any configuration, the same users need to belong to groups + on the provider that are associated with VMS manager user roles for tenant admin type users on the tenant. + type: str + + trash_gid: + description: "GID with permissions to the trash folder" + type: int + + use_smb_native: + description: "Use native SMB authentication" + type: bool + + use_smb_privileged_group: + description: "Enables SMB privileged user group" + type: bool + + use_smb_privileged_user: + description: "Enables SMB privileged user" + type: bool + + force_remove: + description: | + Forces deletion of the tenant, even if files exist in the tenant directory. (only used when state=absent) + type: bool + state: + description: Desired state of the resource. + type: str + choices: [present, absent] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Create Tenant + vastdata.vms.tenants: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-name" + state: present + +- name: Update Tenant + vastdata.vms.tenants: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: present + +- name: Delete Tenant + vastdata.vms.tenants: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: absent +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +tenants: + description: The tenants resource data. + type: dict + returned: always + sample: + id: 1 + name: "example" +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "access_ip_ranges": {"type": "list", "default": None, "elements": "str"}, + "ad_provider_id": {"type": "int", "default": None}, + "allow_disabled_users": {"type": "bool", "default": None}, + "allow_locked_users": {"type": "bool", "default": None}, + "allowed_delegations": { + "type": "str", + "default": None, + "choices": [ + "NONE", + "READ", + "WRITE", + "READ_WRITE", + ], + }, + "application_users_group_name": {"type": "str", "default": None}, + "capacity_rules": {"type": "dict", "default": None}, + "client_ip_ranges": {"type": "list", "default": None, "elements": "list"}, + "data_engine_role_enabled": {"type": "bool", "default": None}, + "data_engine_s3_policy_enabled": {"type": "bool", "default": None}, + "default_others_share_level_perm": { + "type": "str", + "default": None, + "choices": [ + "READ", + "CHANGE", + "FULL", + ], + }, + "domain_name": {"type": "str", "default": None}, + "encryption_crn": {"type": "str", "default": None}, + "encryption_group": {"type": "str", "default": None}, + "grant_unrequested_delegations_by_default": {"type": "bool", "default": None}, + "identity_provider_name": {"type": "str", "default": None}, + "is_nfsv42_supported": {"type": "bool", "default": None}, + "krb_provider_id": {"type": "int", "default": None}, + "ldap_provider_id": {"type": "int", "default": None}, + "local_provider_id": {"type": "int", "default": None}, + "login_name_primary_provider": { + "type": "str", + "default": None, + "choices": [ + "NONE", + "LDAP", + "NIS", + "AD", + "LOCAL", + ], + }, + "max_views": {"type": "int", "default": None}, + "name": {"type": "str", "default": None}, + "nis_provider_id": {"type": "int", "default": None}, + "oidc_provider_id": {"type": "int", "default": None}, + "posix_primary_provider": {"type": "str", "default": None, "choices": ["NONE", "LDAP", "NIS", "AD"]}, + "preferred_owning_group": {"type": "str", "default": None, "choices": ["PROTOCOL_BASED", "POSIX_GID"]}, + "qos": {"type": "dict", "default": None}, + "require_smb_signing": {"type": "bool", "default": None}, + "smb_administrators_group_name": {"type": "str", "default": None}, + "smb_encryption_state": { + "type": "str", + "default": None, + "choices": [ + "OFF", + "AVAILABLE", + "DESIRED", + "REQUIRED", + ], + }, + "smb_privileged_group_full_access": {"type": "bool", "default": None}, + "smb_privileged_group_sid": {"type": "str", "default": None}, + "smb_privileged_user_name": {"type": "str", "default": None}, + "tenant_admins_group_name": {"type": "str", "default": None}, + "trash_gid": {"type": "int", "default": None}, + "use_smb_native": {"type": "bool", "default": None}, + "use_smb_privileged_group": {"type": "bool", "default": None}, + "use_smb_privileged_user": {"type": "bool", "default": None}, + "force_remove": {"type": "bool", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class TenantResource(BaseResource): + """Resource manager for Tenant.""" + + resource_name = "tenants" + singular = "tenant" + lookup_field = "name" + async_delete = True + create_only_fields = {"encryption_crn", "encryption_group", "name"} # Fields only valid during creation + update_only_fields = {"data_engine_role_enabled", "data_engine_s3_policy_enabled"} # Fields only valid during updates + delete_query_params = {"force_remove"} # Sent as URL query params on DELETE + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = TenantResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/user_key.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/user_key.py new file mode 100644 index 000000000..7e8f0150f --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/user_key.py @@ -0,0 +1,324 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# +# Sub-endpoint module: users/access_keys + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: user_key +short_description: Manage user access keys +description: + - Manage user access keys. + - Supports check_mode and diff. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + user_id: + description: "ID of the user" + type: int + required: true + access_key: + description: "The access key to enable" + type: str + + enabled: + description: "Set to true to enable access key pair. Set to false to disable access key pair" + type: bool + + tenant_id: + description: "Tenant ID" + type: int + state: + description: Desired state. + type: str + choices: [present, absent] + default: present +author: + - VAST Data (@vastdata) +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Manage user access keys + vastdata.vms.user_key: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + user_id: 1 +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +result: + description: The operation result. + type: dict + returned: always +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.sub_endpoint_resource import ( + CrudCapability, + SubEndpointResource, +) + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "user_id": {"required": True, "type": "int"}, + "access_key": {"type": "str", "default": None, "no_log": True}, + "enabled": {"type": "bool", "default": None}, + "tenant_id": {"type": "int", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, +} +# END AUTOGENERATED ARGUMENT_SPEC + + +# BEGIN AUTOGENERATED RESOURCE_CLASS +class UserKey(SubEndpointResource): + parent_resource = "users" + sub_path = "access_keys" + path_has_id = True + supported_operations = frozenset({CrudCapability.CREATE, CrudCapability.DELETE, CrudCapability.UPDATE}) + parent_id_param = "user_id" + identity_params = ["access_key", "tenant_id"] + + +# END AUTOGENERATED RESOURCE_CLASS + + +def _apply_user_key_customizations(): # isort: skip + """UserKey-specific overrides applied at import time. + + Wrapped in a function so ansible-doc's AST parser does not choke on + attribute patches at module level. + """ + from typing import Any, Dict, List, Optional + + from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastAPIError + + UserKey.module_result_key = "result" + + def _user_key_run(self) -> None: + state = self.params.get("state", "present") + + if state == "absent": + payload = self._build_payload() + if not self.check_mode: + try: + self._api_base.delete(**payload) + except Exception as e: + self.module.fail_json(msg=f"Failed to delete {self.sub_path}: {e}") + self.module.exit_json(changed=True, result={}) + + access_key = self.params.get("access_key") + if access_key: + current = self._get_current_key() + if current: + self._perform_update(current) + return + + self._perform_create() + + def _user_key_lookup_access_keys(self, user: Dict[str, Any], tenant_id: Optional[int]) -> List[Dict[str, Any]]: + """Return access_keys for the user, scoped to tenant_id when provided.""" + access_keys = list(user.get("access_keys", [])) + + if tenant_id is None: + return access_keys + + identity_params: Dict[str, Any] = {"tenant_id": tenant_id} + if user.get("uid") is not None: + identity_params["uid"] = user["uid"] + elif user.get("name"): + identity_params["username"] = user["name"] + elif user.get("vid") is not None: + identity_params["vid"] = user["vid"] + identity_params["context"] = "udb" + else: + return access_keys + + try: + query_results = self.client.api["users"]["query"].get(**identity_params) + except VastAPIError as e: + self.module.warn(f"Tenant-scoped user query failed, falling back: {e}") + return access_keys + + if not query_results: + return access_keys + + return [{"tenant_id": tenant_id, **k} for k in query_results[0].get("access_keys", [])] + + def _user_key_get_current_key(self) -> Optional[Dict[str, Any]]: + access_key = self.params.get("access_key") + if not access_key: + return None + + try: + user_id = self.params[self.parent_id_param] + users = self.client.api["users"][user_id].get() + if not users: + return None + + user = users[0] + tenant_id = self.params.get("tenant_id") + access_keys = self._lookup_access_keys(user, tenant_id) + + for key in access_keys: + if key.get("access_key") != access_key: + continue + if tenant_id is None or key.get("tenant_id") == tenant_id: + return key + + return None + + except Exception as e: + self.module.warn(f"Could not verify existing key: {e}") + return None + + def _user_key_perform_update(self, current: Dict[str, Any]) -> None: + payload = self._build_payload() + + if not payload: + self.module.exit_json(changed=False, **{self.module_result_key: current}) + + changed = False + for key, value in payload.items(): + if key in self.identity_params: + continue + if current.get(key) != value: + changed = True + break + + if not changed: + self.module.exit_json(changed=False, **{self.module_result_key: current}) + + diff_before = dict(current) + + if not self.check_mode: + try: + result = self.update(payload) + + if not result or not result.get("access_key"): + result = self._get_current_key() or result + + except VastAPIError as e: + self.module.fail_json(msg=str(e)) + diff_after = dict(result) if result else {**diff_before, **payload} + else: + result = {**current, **payload} + diff_after = result + + output = {"changed": True, self.module_result_key: result, "diff": {"before": diff_before, "after": diff_after}} + self.module.exit_json(**output) + + def _user_key_perform_create(self) -> None: + payload = self._build_payload() + + if not self.check_mode: + try: + result = self.create(payload) + except VastAPIError as e: + self.module.fail_json(msg=str(e)) + else: + result = payload + + self.module.exit_json(changed=True, **{self.module_result_key: result}) + + UserKey.run = _user_key_run + UserKey._lookup_access_keys = _user_key_lookup_access_keys + UserKey._get_current_key = _user_key_get_current_key + UserKey._perform_update = _user_key_perform_update + UserKey._perform_create = _user_key_perform_create + + +_apply_user_key_customizations() + + +# BEGIN AUTOGENERATED MAIN +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = UserKey(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/users.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/users.py new file mode 100644 index 000000000..faf4908bd --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/users.py @@ -0,0 +1,266 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: users +short_description: Create, update, or delete VAST User resources +description: + - Create, update, or delete VAST User resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + allow_create_bucket: + description: | + Set to true to give the user permission to create S3 buckets. In case of conflict with an S3 identity policy + attached to the user or to a relevant group, this setting is overridden. + type: bool + + allow_delete_bucket: + description: | + Set to true to give the user permission to delete S3 buckets. In case of conflict with an S3 identity policy + attached to the user or to a relevant group, this setting is overridden. + type: bool + + gids: + description: "List of group GIDs of all groups to which the user should belong." + type: list + elements: int + + leading_gid: + description: "Leading GID" + type: int + + local: + description: "Not in use." + type: bool + + local_provider_id: + description: "The ID of the local provider to which to add the user" + type: int + + name: + description: "User name" + type: str + + password: + description: "Password (Note: This credential is not returned by the API and only used during operations)" + type: str + + s3_policies_ids: + description: "Specify S3 policies to attach to the user." + type: list + elements: int + + s3_superuser: + description: | + Set to true to give the user S3 superuser permission. In case of conflict with an S3 identity policy attached to + the user or to a relevant group, this setting is overridden. + type: bool + + uid: + description: "NFS UID" + type: int + state: + description: Desired state of the resource. + type: str + choices: [present, absent] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Create User + vastdata.vms.users: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-name" + local_provider_id: 1 + state: present + +- name: Update User + vastdata.vms.users: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: present + +- name: Delete User + vastdata.vms.users: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: absent +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +users: + description: The users resource data. + type: dict + returned: always + sample: + id: 1 + name: "example" +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "allow_create_bucket": {"type": "bool", "default": None}, + "allow_delete_bucket": {"type": "bool", "default": None}, + "gids": {"type": "list", "default": None, "elements": "int"}, + "leading_gid": {"type": "int", "default": None}, + "local": {"type": "bool", "default": None}, + "local_provider_id": {"type": "int", "default": None}, + "name": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "s3_policies_ids": {"type": "list", "default": None, "elements": "int"}, + "s3_superuser": {"type": "bool", "default": None}, + "uid": {"type": "int", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class UserResource(BaseResource): + """Resource manager for User.""" + + resource_name = "users" + singular = "user" + lookup_field = "name" + create_only_fields = {"name"} # Fields only valid during creation + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = UserResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/viewpolicies.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/viewpolicies.py new file mode 100644 index 000000000..33a7f81b7 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/viewpolicies.py @@ -0,0 +1,567 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: viewpolicies +short_description: Create, update, or delete VAST ViewPolicy resources +description: + - Create, update, or delete VAST ViewPolicy resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + access_flavor: + description: | + Applicable with MIXED_LAST_WINS security flavor (Access can be set via NFSv3 regardless of this option) + type: str + choices: ["NFS4", "SMB", "ALL"] + + allowed_characters: + description: "Specifies the policy for which characters are allowed in file names." + type: str + choices: ["LCD", "NPL"] + + apple_sid: + description: | + For use when connecting from Mac clients to SMB shares, this option enables Security IDs (SIDs) to be returned in + Apple compatible representation. + type: bool + + atime_frequency: + description: | + Frequency for updating the atime attribute of NFS files. atime is updated on read operations if the difference + between the current time and the file's atime value is greater than the atime frequency. For example: 300 or + 00:00:30 seconds is supported. Zero value is not supported. Default: 3600 + type: str + + auth_source: + description: | + Specifies which source is trusted for the user's group memberships, when users' access to the view is authorized. + type: str + choices: ["RPC", "PROVIDERS", "RPC_AND_PROVIDERS"] + + cluster_id: + description: "Cluster Id" + type: int + + disable_handle_lease: + description: "Disable Handle Lease" + type: bool + + disable_read_lease: + description: "Disable Read Lease" + type: bool + + disable_write_lease: + description: "Disable Write Lease" + type: bool + + enable_access_to_snapshot_dir_in_subdirs: + description: "Specifies whether to make the .snapshot directory accessible in subdirectories of the View." + type: bool + + enable_visibility_of_snapshot_dir: + description: "Specifies whether to make the .snapshot directory visible in subdirectories of the View." + type: bool + + expose_id_in_fsid: + description: "Expose Id In Fsid" + type: bool + + flavor: + description: | + Sets the security flavor, which determines how file and directory permissions are applied in multiprotocol views + type: str + choices: ["NFS", "SMB", "MIXED_LAST_WINS", "S3_NATIVE"] + + gid_inheritance: + description: "GID inheritance. BSD - Inherit the GID from the parent folder. LINUX - Inherit the GID from the user." + type: str + choices: ["BSD", "LINUX"] + + inherit_parent_mode_bits: + description: "Enable NFS behavior of inheriting POSIX settings from the parent directory versus configured values. " + type: bool + + is_s3_default_policy: + description: "Specifies whether to make the view policy the default policy used for S3 endpoint views." + type: bool + + name: + description: "Name" + type: str + + nfs_all_squash: + description: | + Specify which NFS client hosts have all squash. With all squash, all client users are mapped to nobody for all + file and folder management operations on the export. Specify array of hosts separated by commas. Each host can be + specified as an IP address, a netgroup key beginning with @, a CIDR subnet or a range of IPs indicated by an IP + address with a * as a wildcard in place of any of the 8-bit fields in the IP address. + type: list + elements: str + + nfs_case_insensitive: + description: "Force case insensitivity for NFSv3 and NFSv4" + type: bool + + nfs_enforce_tls: + description: | + Accept NFSv3 and NFSv4 client mounts only if they are TLS-encrypted. Use only with Minimal Protection Level set + to System or None. + type: bool + + nfs_enforce_tls_relaxed: + description: | + Whether to relax TLS enforcement by not requiring TLS for auxiliary NFSv3 sub-protocols | (MOUNT, NLM, NSM, + RQUOTA, NFSACL) + type: bool + + nfs_minimal_protection_level: + description: | + For a policy intended for use with NFSv4-enabled views, sets the Minimal Protection Level for NFSv4 client + mounts: 'KRB_AUTH_ONLY' allows client mounts with Kerberos authentication only (using the RPCSEC_GSS + authentication service), 'SYSTEM' allows client mounts using either the AUTH_SYS RCP security flavor (the + traditional default NFS authentication scheme) or with Kerberos authentication, 'NONE' (default) allows client + mounts with the AUTH_NONE (anonymous access), or AUTH_SYS RCP security flavors, or with Kerberos authentication. + type: str + + nfs_no_squash: + description: | + Specify which NFS client hosts have no squash. With no squash, all operations are supported. Use this option if + you trust the root user not to perform operations that will corrupt data. Specify array of hosts separated by + commas. Each host can be specified as an IP address, a netgroup key beginning with @, a CIDR subnet or a range of + IPs indicated by an IP address with a * as a wildcard in place of any of the 8-bit fields in the IP address. + type: list + elements: str + + nfs_posix_acl: + description: "Enables full support of extended POSIX Access Control Lists (ACL)" + type: bool + + nfs_read_only: + description: | + Specify which NFS client hosts can access the view with read-only access. Specify array of hosts separated by + commas. Each host can be specified as an IP address, a netgroup key beginning with @, a CIDR subnet or a range of + IPs indicated by an IP address with a * as a wildcard in place of any of the 8-bit fields in the IP address. + type: list + elements: str + + nfs_read_write: + description: | + Specify which NFS client hosts can access the view with read-write access. Specify array of hosts separated by + commas. Each host can be specified as an IP address, a netgroup key beginning with @, a CIDR subnet or a range of + IPs indicated by an IP address with a * as a wildcard in place of any of the 8-bit fields in the IP address. + type: list + elements: str + + nfs_return_open_permissions: + description: | + If enabled for NFS-exposed views, the NFS server unilaterally returns open (777) permission for all files and + directories when responding to client side access checks + type: bool + + nfs_root_squash: + description: | + Specify which NFS client hosts have root squash. With root squash, the root user is mapped to nobody for all file + and folder management operations on the export. This enables you to prevent the strongest super user from + corrupting all user data on the VAST Cluster. Specify array of hosts separated by commas. Each host can be + specified as an IP address, a netgroup key beginning with @, a CIDR subnet or a range of IPs indicated by an IP + address with a * as a wildcard in place of any of the 8-bit fields in the IP address. + type: list + elements: str + + path_length: + description: "Specifies the policy for limiting file path component name length." + type: str + choices: ["LCD", "NPL"] + + permission_per_vip_pool: + description: "VIP pools permissions map - {vippol_id: permission}. Example - {1: 'RW'}." + type: dict + + protocols: + description: "Array of protocols to audit" + type: list + elements: str + + protocols_audit: + description: | + Specify audit options to enable them for all attached views in addition to auditing options that are enabled + globably on the cluster. + type: dict + + read_only: + description: | + Specify which NFS client hosts can access the view with read-only access. Specify array of hosts separated by + commas. Each host can be specified as an IP address, a netgroup key beginning with @, a CIDR subnet or a range of + IPs indicated by an IP address with a * as a wildcard in place of any of the 8-bit fields in the IP address. + type: list + elements: str + + read_write: + description: | + Specify which NFS client hosts can access the view with read-write access. Specify array of hosts separated by + commas. Each host can be specified as an IP address, a netgroup key beginning with @, a CIDR subnet or a range of + IPs indicated by an IP address with a * as a wildcard in place of any of the 8-bit fields in the IP address. + type: list + elements: str + + s3_bucket_acl: + description: "S3 Bucket Acl" + type: dict + + s3_flavor_allow_free_listing: + description: | + Allow NFS clients freely list bucket views and their subdirectories, regardless of individual object permissions. + type: bool + + s3_flavor_detect_full_pathname: + description: | + When this flag is enabled in S3 flavor, NFS access to objects is determined based on the full resource names + specified in the identity policies. When disabled, only the bucket name is compared to the identity policy. + type: bool + + s3_object_acl: + description: "S3 Object Acl" + type: dict + + s3_read_only: + description: | + Specify which S3 client hosts can access the view with read-only access. Specify array of hosts separated by + commas. Each host can be specified as an IP address, a netgroup key beginning with @, a CIDR subnet or a range of + IPs indicated by an IP address with a * as a wildcard in place of any of the 8-bit fields in the IP address. + type: list + elements: str + + s3_read_write: + description: | + Specify which S3 client hosts can access the view with read-write access. Specify array of hosts separated by + commas. Each host can be specified as an IP address, a netgroup key beginning with @, a CIDR subnet or a range of + IPs indicated by an IP address with a * as a wildcard in place of any of the 8-bit fields in the IP address. + type: list + elements: str + + s3_special_chars_support: + description: | + This will enable object names that contain \"//\" or \"/../\" and are incompatible with other protocols + type: bool + + s3_visibility: + description: | + Users with permission to list buckets that are created using this policy even if they do not have permission to + access those buckets. + type: list + elements: str + + s3_visibility_groups: + description: | + Users with permission to list buckets that are created using this policy even if they do not have permission to + access those buckets. + type: list + elements: str + + smb_directory_mode: + description: | + For multiprotocol views, if the security flavor is NFS, this parameter sets default unix permission bits for + directories created by SMB clients. Use three digit numeric notation, each digit representing the user, group and + others compontents of the permissions, in that order. Each digit is the sum of the read bit, write bit and + execute bit. If reading is permitted, the read bit adds 4 to the component. If writing is permitted, the write + bit adds 2 to the component. If execution is permitted, the execute bit adds 1 to the component. + type: int + + smb_file_mode: + description: | + For multiprotocol views, if the security flavor is NFS, this parameter sets default unix permission bits for + files created by SMB clients. Use three digit numeric notation, each digit representing the user, group and + others compontents of the permissions, in that order. Each digit is the sum of the read bit, write bit and + execute bit. If reading is permitted, the read bit adds 4 to the component. If writing is permitted, the write + bit adds 2 to the component. If execution is permitted, the execute bit adds 1 to the component. + type: int + + smb_is_ca: + description: | + When enabled, the SMB share exposed by the view is set as continuously available, which allows SMB3 clients to + request use of persistent file handles and keep their connections to this share in case of a failover event. + type: bool + + smb_read_only: + description: | + Specify which SMB client hosts can access the view with read-only access. Specify array of hosts separated by + commas. Each host can be specified as an IP address, a netgroup key beginning with @, a CIDR subnet or a range of + IPs indicated by an IP address with a * as a wildcard in place of any of the 8-bit fields in the IP address. + type: list + elements: str + + smb_read_write: + description: | + Specify which SMB client hosts can access the view with read-write access. Specify array of hosts separated by + commas. Each host can be specified as an IP address, a netgroup key beginning with @, a CIDR subnet or a range of + IPs indicated by an IP address with a * as a wildcard in place of any of the 8-bit fields in the IP address. + type: list + elements: str + + tenant_id: + description: "Tenant ID" + type: int + + trash_access: + description: | + Specify which NFSv3 client hosts can access the trash folder. Specify array of hosts separated by commas. Each + host can be specified as an IP address, a netgroup key beginning with @, a CIDR subnet or a range of IPs + indicated by an IP address with a * as a wildcard in place of any of the 8-bit fields in the IP address. Trash + folder access must also be enabled for the cluster. + type: list + elements: str + + use_32bit_fileid: + description: | + Sets the VAST Cluster's NFS server to use 32bit file IDs. This setting supports legacy 32-bit applications + running over NFS. + type: str + + use_auth_provider: + description: "Not in use" + type: bool + state: + description: Desired state of the resource. + type: str + choices: [present, absent] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Create ViewPolicy + vastdata.vms.viewpolicies: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-name" + state: present + +- name: Update ViewPolicy + vastdata.vms.viewpolicies: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: present + +- name: Delete ViewPolicy + vastdata.vms.viewpolicies: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: absent +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +viewpolicies: + description: The viewpolicies resource data. + type: dict + returned: always + sample: + id: 1 + name: "example" +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "access_flavor": {"type": "str", "default": None, "choices": ["NFS4", "SMB", "ALL"]}, + "allowed_characters": {"type": "str", "default": None, "choices": ["LCD", "NPL"]}, + "apple_sid": {"type": "bool", "default": None}, + "atime_frequency": {"type": "str", "default": None}, + "auth_source": {"type": "str", "default": None, "choices": ["RPC", "PROVIDERS", "RPC_AND_PROVIDERS"]}, + "cluster_id": {"type": "int", "default": None}, + "disable_handle_lease": {"type": "bool", "default": None}, + "disable_read_lease": {"type": "bool", "default": None}, + "disable_write_lease": {"type": "bool", "default": None}, + "enable_access_to_snapshot_dir_in_subdirs": {"type": "bool", "default": None}, + "enable_visibility_of_snapshot_dir": {"type": "bool", "default": None}, + "expose_id_in_fsid": {"type": "bool", "default": None}, + "flavor": {"type": "str", "default": None, "choices": ["NFS", "SMB", "MIXED_LAST_WINS", "S3_NATIVE"]}, + "gid_inheritance": {"type": "str", "default": None, "choices": ["BSD", "LINUX"]}, + "inherit_parent_mode_bits": {"type": "bool", "default": None}, + "is_s3_default_policy": {"type": "bool", "default": None}, + "name": {"type": "str", "default": None}, + "nfs_all_squash": {"type": "list", "default": None, "elements": "str"}, + "nfs_case_insensitive": {"type": "bool", "default": None}, + "nfs_enforce_tls": {"type": "bool", "default": None}, + "nfs_enforce_tls_relaxed": {"type": "bool", "default": None}, + "nfs_minimal_protection_level": {"type": "str", "default": None}, + "nfs_no_squash": {"type": "list", "default": None, "elements": "str"}, + "nfs_posix_acl": {"type": "bool", "default": None}, + "nfs_read_only": {"type": "list", "default": None, "elements": "str"}, + "nfs_read_write": {"type": "list", "default": None, "elements": "str"}, + "nfs_return_open_permissions": {"type": "bool", "default": None}, + "nfs_root_squash": {"type": "list", "default": None, "elements": "str"}, + "path_length": {"type": "str", "default": None, "choices": ["LCD", "NPL"]}, + "permission_per_vip_pool": {"type": "dict", "default": None}, + "protocols": {"type": "list", "default": None, "elements": "str"}, + "protocols_audit": {"type": "dict", "default": None}, + "read_only": {"type": "list", "default": None, "elements": "str"}, + "read_write": {"type": "list", "default": None, "elements": "str"}, + "s3_bucket_acl": {"type": "dict", "default": None}, + "s3_flavor_allow_free_listing": {"type": "bool", "default": None}, + "s3_flavor_detect_full_pathname": {"type": "bool", "default": None}, + "s3_object_acl": {"type": "dict", "default": None}, + "s3_read_only": {"type": "list", "default": None, "elements": "str"}, + "s3_read_write": {"type": "list", "default": None, "elements": "str"}, + "s3_special_chars_support": {"type": "bool", "default": None}, + "s3_visibility": {"type": "list", "default": None, "elements": "str"}, + "s3_visibility_groups": {"type": "list", "default": None, "elements": "str"}, + "smb_directory_mode": {"type": "int", "default": None}, + "smb_file_mode": {"type": "int", "default": None}, + "smb_is_ca": {"type": "bool", "default": None}, + "smb_read_only": {"type": "list", "default": None, "elements": "str"}, + "smb_read_write": {"type": "list", "default": None, "elements": "str"}, + "tenant_id": {"type": "int", "default": None}, + "trash_access": {"type": "list", "default": None, "elements": "str"}, + "use_32bit_fileid": {"type": "str", "default": None}, + "use_auth_provider": {"type": "bool", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class ViewpolicyResource(BaseResource): + """Resource manager for ViewPolicy.""" + + resource_name = "viewpolicies" + singular = "viewpolicy" + lookup_field = "name" + update_only_fields = {"s3_bucket_acl"} # Fields only valid during updates + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = ViewpolicyResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/views.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/views.py new file mode 100644 index 000000000..2da5b22c4 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/views.py @@ -0,0 +1,608 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: views +short_description: Create, update, or delete VAST View resources +description: + - Create, update, or delete VAST View resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + abac_tags: + description: "list of ABAC tags" + type: list + elements: str + + abe_max_depth: + description: | + Restricts ABE to a specified path depth. For example, if max depth is 3, ABE does not affect paths deeper than + three levels. If not specified, ABE affects all path depths. + type: int + + abe_protocols: + description: "The protocols for which Access-Based Enumeration (ABE) is enabled" + type: list + elements: str + + alias: + description: "For NFS-enabled views, a view alias for NFSv3 clients." + type: str + + allow_anonymous_access: + description: "Not in use" + type: bool + + allow_s3_anonymous_access: + description: | + Allow S3 anonymous access to S3 bucket. If true, anonymous requests are granted provided that the object ACL + grants access to the All Users group (in S3 Native security flavor) or the permission mode bits on the requested + file and directory path grant access permission to 'others' (in NFS security flavor). + type: bool + + auto_commit: + description: | + Applicable if locking is enabled. Sets the auto-commit time for files that are locked automatically. These files + are locked automatically after the auto-commit period elapses from the time the file is saved. Files locked + automatically are locked for the default-retention-period, after which they are unlocked. Specify as an integer + value followed by a letter for the unit (h - hours, d - days, y - years). Example: 2h (2 hours). + type: str + + bucket: + description: "Not yet implemented" + type: str + + bucket_creators: + description: | + For S3 endpoint views, specify a list of users, by user name, whose bucket create requests use this view. Any + request to create an S3 bucket that is sent by S3 API by a specified user will use this S3 Endpoint view. Users + should not be specified as bucket creators in more than one S3 Endpoint view. Naming a user as a bucket creator + in two S3 Endpoint views will fail the creation of the view with an error. + type: list + elements: str + + bucket_creators_groups: + description: | + For S3 endpoint views, specify a list of groups, by group name, whose bucket create requests use this view. Any + request to create an S3 bucket that is sent by S3 API by a user who belongs to a group listed here will use this + S3 Endpoint view. Take extra care not to duplicate bucket creators through groups: If you specify a group as a + bucket creator group in one view and you also specify a user who belongs to that group as a bucket creator user + in another view, view creation will not fail. Yet, there is a conflict between the two configurations and the + selection of a view for configuring the user's buckets is not predictable. + type: list + elements: str + + bucket_logging: + description: "Bucket Logging" + type: dict + + bucket_owner: + description: "S3 Bucket owner" + type: str + + bucket_owner_type: + description: "Bucket Owner Type" + type: str + choices: ["USER", "ROLE"] + + create_dir: + description: | + Create a directory at the specified path (Note: This credential is not returned by the API and only used during + operations) + type: bool + + create_dir_acl: + description: "Define ACL for the newly created dir" + type: list + elements: dict + + create_dir_mode: + description: "Unix permissions mode for the new dir" + type: int + + default_retention_period: + description: | + Relevant if locking is enabled. Required if s3_locks_retention_mode is set to governance or compliance. Specifies + a default retention period for objects in the bucket. If set, object versions that are placed in the bucket are + automatically protected with the specified retention lock. Otherwise, by default, each object version has no + automatic protection but can be configured with a retention period or legal hold. Specify as an integer followed + by h for hours, d for days, m for months, or y for years. For example: 2d or 1y. + type: str + + event_notifications: + description: "Event Notifications" + type: list + elements: dict + + files_retention_mode: + description: | + Applicable if locking is enabled. The retention mode for new files. For views enabled for NFSv3 or SMB, if + locking is enabled, files_retention_mode must be set to GOVERNANCE or COMPLIANCE. If the view is enabled for S3 + and not for NFSv3 or SMB, files_retention_mode can be set to NONE. If GOVERNANCE, locked files cannot be deleted + or changed. The Retention settings can be shortened or extended by users with sufficient permissions. If + COMPLIANCE, locked files cannot be deleted or changed. Retention settings can be extended, but not shortened, by + users with sufficient permissions. If NONE (S3 only), the retention mode is not set for the view; it is set + individually for each object. + type: str + + indestructible_object_duration: + description: | + Retention period for objects, in days. Each object in the bucket is protected from deletion, overwriting, + renaming and metadata changes for the specified number of days after its creation date. + type: int + + inherit_acl: + description: "Indicates whether the directory should inherit ACLs from its parent directory" + type: bool + + is_default_subsystem: + description: | + Set to true to set view to be the default subsystem for block storage. There can be up to one default subsystem + per tenant. The default subsystem is the default view selected when creating a block volume if no view is + specified. + type: bool + + is_indestructible_object_enabled: + description: | + Set to true to enable indestructible object mode on the view. This is supported only if S3 is the only specified + protocol. Other limitations also apply. + type: bool + + is_kafka_encrypted_conn_allowed: + description: "True if encrypted connection is allowed for Kafka" + type: bool + + is_kafka_unencrypted_conn_allowed: + description: "True if unencrypted connection is allowed for Kafka" + type: bool + + is_seamless: + description: | + Supports seamless failover between replication peers by syncing file handles between the view and remote views on + the replicated path on replication peers. This enables NFSv3 client users to retain the same mount point to the + view in the event of a failover of the view path to a replication peer. This feature enables NFSv3 client users + to retain the same mount point to the view in the event of a failover of the view path to a replication peer. + Enabling this option may cause overhead and should only be enabled when the use case is relevant. To complete the + configuration for seamless failover between any two peers, a seamless view must be created on each peer. + type: bool + + kafka_encrypted_auth_mechanism: + description: "Authentication mechanism for encrypted connection" + type: str + choices: ["NONE", "SASL_PLAIN"] + + kafka_first_join_group_timeout_sec: + description: "Kafka first join group timeout, in seconds" + type: int + + kafka_is_authorization_required: + description: "True if authorization is required for Kafka" + type: bool + + kafka_rejoin_group_timeout_sec: + description: "Kafka rejoin group timeout, in seconds" + type: int + + kafka_unencrypted_auth_mechanism: + description: "Authentication mechanism for unencrypted connection" + type: str + choices: ["NONE", "SASL_PLAIN"] + + kafka_vip_pools: + description: | + For Kafka-enabled views, an array of IDs of Virtual IP pools used to access event topics exposed by the view. The + specified virtual IP pool must belong to the same tenant as the Kafka-enabled view. Must also not be a virtual IP + pool that is excluded by the view policy's virtual IP pool association. + type: list + elements: int + + locking: + description: | + Set to true to enable object locking on an S3 bucket. Object locking cannot be disabled after the view is + modified. Must be true if s3_versioning is true. + type: bool + + max_retention_period: + description: | + Applicable if locking is enabled. Sets a maximum retention period for files that are locked in the view. Files + cannot be locked for longer than this period, whether they are locked manually (by setting the atime) or + automatically, using auto-commit. Specify as an integer value followed by a letter for the unit (m - minutes, h - + hours, d - days, y - years). Example: 2y (2 years). + type: str + + min_retention_period: + description: | + Applicable if locking is enabled. Sets a minimum retention period for files that are locked in the view. Files + cannot be locked for less than this period, whether locked manually (by setting the atime) or automatically, + using auto-commit. Specify as an integer value followed by a letter for the unit (h - hours, d - days, m - + months, y - years). Example: 1d (1 day). + type: str + + name: + description: "View name" + type: str + + nfs_interop_flags: + description: "Indicates whether the view should support simultaneous access to NFS3/NFS4/SMB protocols." + type: str + choices: + - BOTH_NFS3_AND_NFS4_INTEROP_DISABLED + - ONLY_NFS3_INTEROP_ENABLED + - ONLY_NFS4_INTEROP_ENABLED + - BOTH_NFS3_AND_NFS4_INTEROP_ENABLED + + owner: + description: | + The owner of the folder. Specify the owner using the attribute type set by owner_type. You can specify a group as + the owner, as supported by SMB. To enable setting a group as the owner, set owner_is_group=true. In all cases, + set owning_group also. + type: str + + owner_is_group: + description: | + Set to true if passing a group as the owner of the folder. This feature is used to enable setting a group as the + owner, as supported by SMB. + type: bool + + owner_type: + description: "The type of attribute used to specify owner." + type: str + + owning_group: + description: "The owning group of the folder." + type: str + + owning_group_type: + description: "The type of attribute to use to specify the owning group of the folder." + type: str + + path: + description: "View path" + type: str + + policy_id: + description: | + View policy ID. Specify to change which view policy the view uses. Every view must be attached to one view + policy, which specifies further configurations. + type: int + + protocols: + description: | + Client protocols enabled for access to the view. 'NFS' enables access from NFS version 3, 'NFS4' enables access + from NFS version 4.1 and 4.2, S3' creates an S3 bucket on the view, 'ENDPOINT' creates an S3 endpoint, used as + template for views created via S3 RPCs, DATABASE exposes the view as a VAST database. KAFKA enables events + related to elements on the view path to be published to the VAST Event Broker. BLOCK exposes the view as a block + storage subsystem. + type: list + elements: str + + qos_policy: + description: "QoS Policy" + type: str + + qos_policy_id: + description: "Associates a QoS policy with the view." + type: int + + s3_locks_retention_mode: + description: | + The retention mode for new object versions stored in this bucket. You can override this if you upload a new + object version with an explicit retention mode and period. + type: str + + s3_object_ownership_rule: + description: "S3 Object Ownership Rule" + type: str + + s3_unverified_lookup: + description: "S3 Unverified Lookup" + type: bool + + s3_versioning: + description: "Enable S3 Versioning if S3 bucket. Versioning cannot be disabled after the view is modified." + type: bool + + select_for_live_monitoring: + description: | + Enables live monitoring on the view. Live monitoring can be enabled for up to ten views at one time. Analytics + data for views is polled every 5 minutes by default and every 10 seconds with live monitoring. + type: bool + + share: + description: "SMB share name" + type: str + + share_acl: + description: "Share-level ACL details" + type: dict + + smb_encryption_state: + description: "Defines the encryption level for SMB" + type: str + choices: ["AVAILABLE", "DESIRED", "REQUIRED"] + + tenant_id: + description: "Associates the specified tenant with the view." + type: int + + user_impersonation: + description: "User Impersonation" + type: dict + + force: + description: "Force View removal (only used when state=absent)" + type: bool + state: + description: Desired state of the resource. + type: str + choices: [present, absent] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Create View + vastdata.vms.views: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + path: "example-path" + policy_id: 1 + state: present + +- name: Update View + vastdata.vms.views: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + path: "example-value" + state: present + +- name: Delete View + vastdata.vms.views: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + path: "example-value" + state: absent +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +views: + description: The views resource data. + type: dict + returned: always + sample: + id: 1 + path: "example" +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "abac_tags": {"type": "list", "default": None, "elements": "str"}, + "abe_max_depth": {"type": "int", "default": None}, + "abe_protocols": {"type": "list", "default": None, "elements": "str"}, + "alias": {"type": "str", "default": None}, + "allow_anonymous_access": {"type": "bool", "default": None}, + "allow_s3_anonymous_access": {"type": "bool", "default": None}, + "auto_commit": {"type": "str", "default": None}, + "bucket": {"type": "str", "default": None}, + "bucket_creators": {"type": "list", "default": None, "elements": "str"}, + "bucket_creators_groups": {"type": "list", "default": None, "elements": "str"}, + "bucket_logging": {"type": "dict", "default": None}, + "bucket_owner": {"type": "str", "default": None}, + "bucket_owner_type": {"type": "str", "default": None, "choices": ["USER", "ROLE"]}, + "create_dir": {"type": "bool", "default": None, "no_log": True}, + "create_dir_acl": {"type": "list", "default": None, "elements": "dict"}, + "create_dir_mode": {"type": "int", "default": None}, + "default_retention_period": {"type": "str", "default": None}, + "event_notifications": {"type": "list", "default": None, "elements": "dict"}, + "files_retention_mode": {"type": "str", "default": None}, + "indestructible_object_duration": {"type": "int", "default": None}, + "inherit_acl": {"type": "bool", "default": None}, + "is_default_subsystem": {"type": "bool", "default": None}, + "is_indestructible_object_enabled": {"type": "bool", "default": None}, + "is_kafka_encrypted_conn_allowed": {"type": "bool", "default": None}, + "is_kafka_unencrypted_conn_allowed": {"type": "bool", "default": None}, + "is_seamless": {"type": "bool", "default": None}, + "kafka_encrypted_auth_mechanism": {"type": "str", "default": None, "choices": ["NONE", "SASL_PLAIN"]}, + "kafka_first_join_group_timeout_sec": {"type": "int", "default": None}, + "kafka_is_authorization_required": {"type": "bool", "default": None}, + "kafka_rejoin_group_timeout_sec": {"type": "int", "default": None}, + "kafka_unencrypted_auth_mechanism": {"type": "str", "default": None, "choices": ["NONE", "SASL_PLAIN"]}, + "kafka_vip_pools": {"type": "list", "default": None, "elements": "int"}, + "locking": {"type": "bool", "default": None}, + "max_retention_period": {"type": "str", "default": None}, + "min_retention_period": {"type": "str", "default": None}, + "name": {"type": "str", "default": None}, + "nfs_interop_flags": { + "type": "str", + "default": None, + "choices": [ + "BOTH_NFS3_AND_NFS4_INTEROP_DISABLED", + "ONLY_NFS3_INTEROP_ENABLED", + "ONLY_NFS4_INTEROP_ENABLED", + "BOTH_NFS3_AND_NFS4_INTEROP_ENABLED", + ], + }, + "owner": {"type": "str", "default": None}, + "owner_is_group": {"type": "bool", "default": None}, + "owner_type": {"type": "str", "default": None}, + "owning_group": {"type": "str", "default": None}, + "owning_group_type": {"type": "str", "default": None}, + "path": {"type": "str", "default": None}, + "policy_id": {"type": "int", "default": None}, + "protocols": {"type": "list", "default": None, "elements": "str"}, + "qos_policy": {"type": "str", "default": None}, + "qos_policy_id": {"type": "int", "default": None}, + "s3_locks_retention_mode": {"type": "str", "default": None}, + "s3_object_ownership_rule": {"type": "str", "default": None}, + "s3_unverified_lookup": {"type": "bool", "default": None}, + "s3_versioning": {"type": "bool", "default": None}, + "select_for_live_monitoring": {"type": "bool", "default": None}, + "share": {"type": "str", "default": None}, + "share_acl": {"type": "dict", "default": None}, + "smb_encryption_state": {"type": "str", "default": None, "choices": ["AVAILABLE", "DESIRED", "REQUIRED"]}, + "tenant_id": {"type": "int", "default": None}, + "user_impersonation": {"type": "dict", "default": None}, + "force": {"type": "bool", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class ViewResource(BaseResource): + """Resource manager for View.""" + + resource_name = "views" + singular = "view" + lookup_field = "path" + create_only_fields = { + "abac_tags", + "cluster_id", + "create_dir_acl", + "create_dir_mode", + "inherit_acl", + "kafka_vip_pools", + "owner", + "owner_is_group", + "owner_type", + "owning_group", + "owning_group_type", + "tenant_id", + } # Fields only valid during creation + delete_query_params = {"force"} # Sent as URL query params on DELETE + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = ViewResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/plugins/modules/vippools.py b/vendor/ansible_collections/vastdata/vms/plugins/modules/vippools.py new file mode 100644 index 000000000..78136a30b --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/plugins/modules/vippools.py @@ -0,0 +1,354 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VAST Data +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +# BEGIN AUTOGENERATED DOCUMENTATION +DOCUMENTATION = r""" +--- +module: vippools +short_description: Create, update, or delete VAST VIPPool resources +description: + - Create, update, or delete VAST VIPPool resources. + - Supports check_mode and diff for idempotent operations. +version_added: "1.0.0" +options: + vms: + description: VAST VMS connection parameters. + type: dict + required: true + suboptions: + host: + description: VAST VMS hostname or IP address. + required: true + type: str + validate_certs: + description: Validate SSL certificates. + type: bool + default: true + timeout: + description: Request timeout in seconds. + type: int + token: + description: API token (VAST 5.3+). Mutually exclusive with username/password. + type: str + username: + description: Username for authentication. Mutually exclusive with token. + type: str + password: + description: Password for authentication. Mutually exclusive with token. + type: str + tenant: + description: Tenant name (optional). + type: str + api_version: + description: API version (optional). Defaults to 'latest' if not specified. + type: str + debug: + description: Enable HTTP debug traces. Traces are emitted as warnings on failure. + type: bool + default: false + id: + description: Resource ID for direct lookup. Mutually exclusive with name-based identification. + type: int + bgp_config_id: + description: "BGP configuration ID for the VIP pool" + type: int + + client_monitoring_ips: + description: "External client monitoring IP ranges for VIP pool connectivity monitoring" + type: list + elements: list + + cluster_id: + description: "Cluster Id" + type: int + + cnode_ids: + description: | + Dedicates a specific group of CNodes to the VIP pool. List the IDs of the CNodes. Separate IDs by commas. This is + a way to dedicate a specific set of CNodes to a specific set of client hosts or applications. Overridden if + cnode_names is passed. + type: list + elements: int + + cnode_names: + description: | + Dedicates a specific group of CNodes to the VIP pool. List the names of the CNodes. Separate names by commas. + This is a way to dedicate a specific set of CNodes to a specific set of client hosts or applications. Overrides + cnode_ids. + type: str + + domain_name: + description: | + Domain name for the VAST DNS server. If a DNS configuration exists, the domain suffix defined in the DNS server + configuration is appended to this domain name to form a FQDN which the DNS server resolves to this VIP pool. + type: str + + enable_l3: + description: | + Enables L3 networking, in which the cluster's CNodes act as Border Gateway Protocol (BGP) peers belonging to a + VAST Autonomous system (AS) which is connected to the client network's AS + type: bool + + enable_weighted_balancing: + description: "Enable weighted balancing" + type: bool + + enabled: + description: "True for enable, False for disable" + type: bool + + end_ip: + description: "Not currently in use. Use ip_ranges instead." + type: str + + gw_ip: + description: "The IP address of a local gateway device if client traffic is routed through one." + type: str + + gw_ipv6: + description: "The IP address of a local gateway device if client traffic is routed through one." + type: str + + ip_ranges: + description: "An array of IP ranges included in the vippool." + type: list + elements: list + + name: + description: "The VIP pool name" + type: str + + peer_asn: + description: "Peer Asn" + type: int + + port_membership: + description: | + Allocates left, right or all CNode ports to the VIP pool. Allocating the left port and the right port in + different VIP pools enables the CNodes to be connected to multiple networks simultaneously. Default: all + type: str + + role: + description: | + 'PROTOCOLS' dedicates the VIP pool to client traffic from all of the supported access protocols (NFSv3, NFSv4.2, + SMBv2, S3, Database). At least one VIP pool must be created to enable client access. 'REPLICATION' dedicates the + VIP pool for connectivity with an async replication peer cluster. This is needed for async replication. + 'BIG_CATALOG' dedicates the VIP pool to VAST Catalog query access from the client network. + type: str + choices: ["PROTOCOLS", "REPLICATION", "BIG_CATALOG", "QUERY_ENGINE_CNODE_GROUP"] + + start_ip: + description: "Not currently in use. Use ip_ranges instead." + type: str + + subnet_cidr: + description: "The subnet expressed as a CIDR index (number of bits in each IP that belong to the subnet)" + type: int + + subnet_cidr_ipv6: + description: "The subnet expressed as a CIDR index (number of bits in each IP that belong to the subnet)" + type: int + + tenant_id: + description: "Tenant ID" + type: int + + vast_asn: + description: "Vast Asn" + type: int + + vlan: + description: | + To tag the VIP pool with a specific VLAN on the data network, specify the VLAN (0-4096). The VIP pool will be + exposed only to the specified VLAN on the client network. + type: int + + vms_preferred: + description: "If true, CNodes participating in the vip pool to be preferred in VMS host election." + type: bool + state: + description: Desired state of the resource. + type: str + choices: [present, absent] + default: present + wait: + description: Wait for async operations to complete. + type: bool + default: true + wait_timeout: + description: Timeout in seconds for async operations. + type: int + default: 300 +author: + - VAST Data (@vastdata) +extends_documentation_fragment: + - ansible.builtin.action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: full + platform: + platforms: posix +""" +# END AUTOGENERATED DOCUMENTATION + +# BEGIN AUTOGENERATED EXAMPLES +EXAMPLES = r""" +- name: Create VIPPool + vastdata.vms.vippools: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + subnet_cidr: 1 + ip_ranges: [] + state: present + +- name: Update VIPPool + vastdata.vms.vippools: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: present + +- name: Delete VIPPool + vastdata.vms.vippools: + vms: + host: vast-vms.example.com + token: "{{ vast_token }}" + name: "example-value" + state: absent +""" +# END AUTOGENERATED EXAMPLES + +# BEGIN AUTOGENERATED RETURN +RETURN = r""" +changed: + description: Whether a change was made. + type: bool + returned: always +vippools: + description: The vippools resource data. + type: dict + returned: always + sample: + id: 1 + name: "example" +diff: + description: Before/after diff when changed. + type: dict + returned: when changed +""" +# END AUTOGENERATED RETURN + +# BEGIN AUTOGENERATED IMPORTS +from typing import Any, Dict + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vastdata.vms.plugins.module_utils.vast.errors import VastError +from ansible_collections.vastdata.vms.plugins.module_utils.vast.resource import BaseResource + +# END AUTOGENERATED IMPORTS + +# BEGIN AUTOGENERATED ARGUMENT_SPEC +ARGUMENT_SPEC: Dict[str, Any] = { + "vms": { + "type": "dict", + "required": True, + "options": { + "host": {"required": True, "type": "str"}, + "validate_certs": {"type": "bool", "default": True}, + "timeout": {"type": "int", "default": None}, + "token": {"type": "str", "default": None, "no_log": True}, + "username": {"type": "str", "default": None}, + "password": {"type": "str", "default": None, "no_log": True}, + "tenant": {"type": "str", "default": None}, + "api_version": {"type": "str", "default": None}, + "debug": {"type": "bool", "default": False}, + }, + "mutually_exclusive": [ + ("token", "username"), + ("token", "password"), + ], + }, + "id": {"type": "int", "default": None}, + "bgp_config_id": {"type": "int", "default": None}, + "client_monitoring_ips": {"type": "list", "default": None, "elements": "list"}, + "cluster_id": {"type": "int", "default": None}, + "cnode_ids": {"type": "list", "default": None, "elements": "int"}, + "cnode_names": {"type": "str", "default": None}, + "domain_name": {"type": "str", "default": None}, + "enable_l3": {"type": "bool", "default": None}, + "enable_weighted_balancing": {"type": "bool", "default": None}, + "enabled": {"type": "bool", "default": None}, + "end_ip": {"type": "str", "default": None}, + "gw_ip": {"type": "str", "default": None}, + "gw_ipv6": {"type": "str", "default": None}, + "ip_ranges": {"type": "list", "default": None, "elements": "list"}, + "name": {"type": "str", "default": None}, + "peer_asn": {"type": "int", "default": None}, + "port_membership": {"type": "str", "default": None}, + "role": { + "type": "str", + "default": None, + "choices": [ + "PROTOCOLS", + "REPLICATION", + "BIG_CATALOG", + "QUERY_ENGINE_CNODE_GROUP", + ], + }, + "start_ip": {"type": "str", "default": None}, + "subnet_cidr": {"type": "int", "default": None}, + "subnet_cidr_ipv6": {"type": "int", "default": None}, + "tenant_id": {"type": "int", "default": None}, + "vast_asn": {"type": "int", "default": None}, + "vlan": {"type": "int", "default": None}, + "vms_preferred": {"type": "bool", "default": None}, + "state": {"type": "str", "choices": ["present", "absent"], "default": "present"}, + "wait": {"type": "bool", "default": True}, + "wait_timeout": {"type": "int", "default": 300}, +} +# END AUTOGENERATED ARGUMENT_SPEC + +# BEGIN AUTOGENERATED RESOURCE_CLASS + + +class VippoolResource(BaseResource): + """Resource manager for VIPPool.""" + + resource_name = "vippools" + singular = "vippool" + lookup_field = "name" + create_only_fields = {"cluster_id", "enable_l3"} # Fields only valid during creation + + +# END AUTOGENERATED RESOURCE_CLASS + +# BEGIN AUTOGENERATED MAIN + + +def main() -> None: + """Module entry point.""" + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + supports_check_mode=True, + ) + + try: + resource = VippoolResource(module) + resource.run() + except VastError as e: + module.fail_json(msg=str(e)) + + +if __name__ == "__main__": + main() +# END AUTOGENERATED MAIN diff --git a/vendor/ansible_collections/vastdata/vms/tests/.gitignore b/vendor/ansible_collections/vastdata/vms/tests/.gitignore new file mode 100644 index 000000000..028ee3bdb --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/.gitignore @@ -0,0 +1,2 @@ +# Ansible test output directories +output/ diff --git a/vendor/ansible_collections/vastdata/vms/tests/__init__.py b/vendor/ansible_collections/vastdata/vms/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/requirements.txt b/vendor/ansible_collections/vastdata/vms/tests/integration/requirements.txt new file mode 100644 index 000000000..42e887eed --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/requirements.txt @@ -0,0 +1,16 @@ +# Integration test requirements +# ansible-core 2.19.x is the last version that supports Python 3.11 +# (2.20.x requires Python 3.12+) +ansible-core>=2.19.0,<2.20.0 +cffi==2.0.0 +cryptography==46.0.4 +Jinja2==3.1.6 +MarkupSafe==3.0.3 +packaging==26.0 +pathspec==1.0.4 +pycparser==3.0 +PyYAML==6.0.3 +requests==2.32.5 +resolvelib==1.2.1 +urllib3==1.26.20 +yamllint==1.38.0 diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/.gitkeep b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/cnodes/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/cnodes/defaults/main.yml new file mode 100644 index 000000000..5cb3669a8 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/cnodes/defaults/main.yml @@ -0,0 +1,17 @@ +--- +# Default variables for cnodes integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +# Leave empty to skip tests gracefully +vast_vms: "" +vast_username: "" +vast_password: "" + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/cnodes/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/cnodes/tasks/main.yml new file mode 100644 index 000000000..2a4dab8bd --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/cnodes/tasks/main.yml @@ -0,0 +1,261 @@ +--- +# Integration tests for cnodes module +# Full lifecycle: discover -> check_mode -> idempotency -> delete -> create -> disable -> enable -> restore +# Runs on ALL cluster types (loopback and real multi-node) +# +# Configuration: +# Set values in tests/integration/integration_config.yml + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials | bool }}" + +# ==================== +# Discover CNode +# ==================== + +- name: Fetch list of existing cnodes from API (auto-discovery) + ansible.builtin.uri: + url: "https://{{ vast_vms }}/api/cnodes/" + method: GET + user: "{{ vast_username }}" + password: "{{ vast_password }}" + force_basic_auth: true + validate_certs: false + status_code: 200 + register: cnodes_list + when: can_run_tests | bool + +- name: Set cnode facts from auto-discovery + ansible.builtin.set_fact: + discovered_cnode_id: "{{ cnodes_list.json[0].id }}" + original_cnode_enabled: "{{ cnodes_list.json[0].enabled }}" + discovered_cnode_cluster_id: "{{ cnodes_list.json[0].cluster_id }}" + discovered_cnode_ip: "{{ cnodes_list.json[0].ip }}" + when: can_run_tests | bool and cnodes_list.json is defined and cnodes_list.json | length > 0 + +- name: Determine whether cnode tests can run + ansible.builtin.set_fact: + can_test_cnode: "{{ can_run_tests | bool and discovered_cnode_id is defined }}" + +# ==================== +# Check Mode Tests (non-destructive, run first while cluster is healthy) +# ==================== + +- name: Test check mode - update enabled to opposite of current state + vastdata.vms.cnodes: + vms: "{{ vms_auth }}" + id: "{{ discovered_cnode_id }}" + enabled: "{{ not (original_cnode_enabled | bool) }}" + state: present + check_mode: true + register: result_check + when: can_test_cnode | bool + +- name: Verify check mode reported the expected change + ansible.builtin.assert: + that: + - result_check is changed + - result_check.cnodes is defined + when: result_check is not skipped + +- name: Verify check mode did not actually apply changes + vastdata.vms.cnodes: + vms: "{{ vms_auth }}" + id: "{{ discovered_cnode_id }}" + enabled: "{{ original_cnode_enabled | bool }}" + state: present + register: result_no_change + when: can_test_cnode | bool + +- name: Verify cnode state is unchanged after check mode + ansible.builtin.assert: + that: + - result_no_change is not changed + when: result_no_change is not skipped + +# ==================== +# Idempotency Test (non-destructive) +# ==================== + +- name: Test idempotency - set enabled to same value as current + vastdata.vms.cnodes: + vms: "{{ vms_auth }}" + id: "{{ discovered_cnode_id }}" + enabled: "{{ original_cnode_enabled | bool }}" + state: present + register: result_idempotent + when: can_test_cnode | bool + +- name: Verify idempotency (should not change) + ansible.builtin.assert: + that: + - result_idempotent is not changed + when: result_idempotent is not skipped + +# ==================== +# Delete Test (remove cnode from cluster) +# ==================== + +- name: Test delete - remove the discovered cnode + vastdata.vms.cnodes: + vms: "{{ vms_auth }}" + id: "{{ discovered_cnode_id }}" + state: absent + register: result_delete + when: can_test_cnode | bool + +- name: Verify cnode was deleted + ansible.builtin.assert: + that: + - result_delete is changed + when: result_delete is not skipped + +# ==================== +# Create Test (re-add cnode by IP) +# ==================== + +- name: Test create - re-add the cnode by IP + vastdata.vms.cnodes: + vms: "{{ vms_auth }}" + ip: "{{ discovered_cnode_ip }}" + cluster_id: "{{ discovered_cnode_cluster_id }}" + state: present + register: result_create + when: can_test_cnode | bool + +- name: Verify cnode was created + ansible.builtin.assert: + that: + - result_create is changed + - result_create.cnodes is defined + - result_create.cnodes.id is defined + when: result_create is not skipped + +- name: Save re-created cnode ID + ansible.builtin.set_fact: + recreated_cnode_id: "{{ result_create.cnodes.id }}" + when: result_create is not skipped and result_create is not failed + +# ==================== +# Disable Test (enabled=false) +# ==================== + +- name: Test disable - set enabled to false + vastdata.vms.cnodes: + vms: "{{ vms_auth }}" + id: "{{ recreated_cnode_id | default(discovered_cnode_id) }}" + enabled: false + state: present + register: result_disable + when: can_test_cnode | bool + +- name: Verify cnode was disabled + ansible.builtin.assert: + that: + - result_disable is changed + - result_disable.cnodes is defined + when: result_disable is not skipped + +# ==================== +# Enable Test with Diff Mode (enabled=true, restore) +# ==================== + +- name: Test enable with diff mode - set enabled to true + vastdata.vms.cnodes: + vms: "{{ vms_auth }}" + id: "{{ recreated_cnode_id | default(discovered_cnode_id) }}" + enabled: true + state: present + diff: true + register: result_enable + when: can_test_cnode | bool + +- name: Verify cnode was enabled and diff data returned + ansible.builtin.assert: + that: + - result_enable is changed + - result_enable.diff is defined + - result_enable.diff.before is defined + - result_enable.diff.after is defined + when: result_enable is not skipped + +# ==================== +# Error Handling Tests +# ==================== + +- name: Test error handling - invalid state value + vastdata.vms.cnodes: + vms: "{{ vms_auth }}" + id: "{{ recreated_cnode_id | default(discovered_cnode_id) | default(1) }}" + state: invalid + register: result + ignore_errors: true + when: can_run_tests | bool + +- name: Verify error was caught + ansible.builtin.assert: + that: + - result is failed + - "'invalid' in result.msg" + when: result is not skipped + +# ==================== +# Safety Net +# ==================== +# Ensure the cnode is back and enabled no matter what failed above. + +- name: Safety net - re-add cnode if it was deleted but not re-created + vastdata.vms.cnodes: + vms: "{{ vms_auth }}" + ip: "{{ discovered_cnode_ip }}" + cluster_id: "{{ discovered_cnode_cluster_id }}" + state: present + when: can_test_cnode | bool and recreated_cnode_id is not defined + failed_when: false + +- name: Safety net - re-enable cnode if it was disabled + vastdata.vms.cnodes: + vms: "{{ vms_auth }}" + id: "{{ recreated_cnode_id | default(discovered_cnode_id) }}" + enabled: true + state: present + when: can_test_cnode | bool + failed_when: false + +# ==================== +# Skip Messages +# ==================== + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping cnodes integration tests - missing credentials. + + To enable integration tests, set the following in: + tests/integration/integration_config.yml + + VAST credentials: + vast_vms: "vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration cnodes -v + when: not (can_run_tests | bool) + +- name: Skip message when no cnodes found in the cluster + ansible.builtin.debug: + msg: | + Skipping cnode-specific tests - no CNodes found in the cluster. + + CNodes represent physical compute nodes; tests require at least + one existing CNode to be discovered via the API. + when: can_run_tests | bool and not (can_test_cnode | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/dns/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/dns/defaults/main.yml new file mode 100644 index 000000000..8bc7f604a --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/dns/defaults/main.yml @@ -0,0 +1,16 @@ +--- +# Default variables for DNS integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +vast_vms: "" +vast_username: "" +vast_password: "" + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/dns/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/dns/tasks/main.yml new file mode 100644 index 000000000..21f250716 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/dns/tasks/main.yml @@ -0,0 +1,185 @@ +--- +# Integration tests for dns module + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials }}" + +# ==================== +# Cleanup - Remove existing DNS configurations if present +# ==================== + +- name: Get list of all DNS configurations + ansible.builtin.uri: + url: "https://{{ vast_vms }}/api/latest/dns/" + method: GET + user: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + force_basic_auth: true + register: dns_list + when: can_run_tests | bool + failed_when: false + +- name: Clean up - remove all existing DNS configurations before starting + vastdata.vms.dns: + vms: "{{ vms_auth }}" + id: "{{ item.id }}" + state: absent + loop: "{{ dns_list.json | default([]) }}" + when: can_run_tests | bool and dns_list.json is defined and dns_list.json | length > 0 + failed_when: false + +# ==================== +# Create Tests +# ==================== + +- name: Test creating DNS configuration + vastdata.vms.dns: + vms: "{{ vms_auth }}" + name: "ansible-test-dns" + domain_suffix: "ansible-test.local" + enabled: true + port: 53 + ttl: 300 + vip: "192.168.1.100" # Required: at least one VIP type must be provided + vip_subnet_cidr: 24 + vip_gateway: "192.168.1.1" + vip_vlan: 100 + state: present + register: result + when: can_run_tests | bool + +- name: Verify DNS creation succeeded + ansible.builtin.assert: + that: + - result is changed + - result.dns is defined + - result.dns.name == "ansible-test-dns" + - result.dns.domain_suffix == "ansible-test.local" + when: result is not skipped + +# ==================== +# Idempotency Tests +# ==================== + +- name: Test idempotency - create same DNS again + vastdata.vms.dns: + vms: "{{ vms_auth }}" + name: "ansible-test-dns" + domain_suffix: "ansible-test.local" + enabled: true + port: 53 + ttl: 300 + vip: "192.168.1.100" + vip_subnet_cidr: 24 + vip_gateway: "192.168.1.1" + vip_vlan: 100 + state: present + register: result + when: can_run_tests | bool + +- name: Verify idempotency (should not change) + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Update Tests +# ==================== + +- name: Test updating DNS configuration + vastdata.vms.dns: + vms: "{{ vms_auth }}" + name: "ansible-test-dns" + domain_suffix: "ansible-test.local" + enabled: true + port: 53 + ttl: 600 # Changed from 300 + vip: "192.168.1.100" + vip_subnet_cidr: 24 + vip_gateway: "192.168.1.1" + vip_vlan: 100 + state: present + register: result + when: can_run_tests | bool + +- name: Verify DNS was updated + ansible.builtin.assert: + that: + - result is changed + - result.dns is defined + - result.dns.ttl == 600 + when: result is not skipped + +- name: Test update idempotency + vastdata.vms.dns: + vms: "{{ vms_auth }}" + name: "ansible-test-dns" + domain_suffix: "ansible-test.local" + enabled: true + port: 53 + ttl: 600 + vip: "192.168.1.100" + vip_subnet_cidr: 24 + vip_gateway: "192.168.1.1" + vip_vlan: 100 + state: present + register: result + when: can_run_tests | bool + +- name: Verify update idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Delete Tests +# ==================== + +- name: Test deleting DNS configuration + vastdata.vms.dns: + vms: "{{ vms_auth }}" + name: "ansible-test-dns" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify DNS was deleted + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Test delete idempotency - delete again + vastdata.vms.dns: + vms: "{{ vms_auth }}" + name: "ansible-test-dns" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Cleanup +# ==================== + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: "Skipping DNS integration tests - credentials not configured" + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/eventdefinitionconfigs/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/eventdefinitionconfigs/defaults/main.yml new file mode 100644 index 000000000..4c52f659f --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/eventdefinitionconfigs/defaults/main.yml @@ -0,0 +1,18 @@ +--- +# Default variables for eventdefinitionconfigs integration tests. +# Override in tests/integration/integration_config.yml or via environment. + +vast_vms: "" +vast_username: "" +vast_password: "" + +# Global event definition config ID (typically 1; override if cluster uses another) +eventdefinitionconfig_id: 1 + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/eventdefinitionconfigs/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/eventdefinitionconfigs/tasks/main.yml new file mode 100644 index 000000000..d9469794a --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/eventdefinitionconfigs/tasks/main.yml @@ -0,0 +1,282 @@ +--- +# Integration tests for eventdefinitionconfigs module. +# eventdefinitionconfigs is update-only (no create/delete). Global config typically has id=1. +# +# Configuration: Set values in tests/integration/integration_config.yml + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials | bool }}" + +# ==================== +# eventdefinitionconfigs – read (idempotent present) +# ==================== + +- name: Read event definition config (no change) + vastdata.vms.eventdefinitionconfigs: + vms: "{{ vms_auth }}" + id: "{{ eventdefinitionconfig_id }}" + state: present + register: read_result + when: can_run_tests | bool + +- name: Verify event definition config was read + ansible.builtin.assert: + that: + - read_result.eventdefinitionconfigs is defined + - read_result.eventdefinitionconfigs.id is defined + when: can_run_tests | bool + +- name: Save current info_value for restore later + ansible.builtin.set_fact: + saved_info_value: "{{ read_result.eventdefinitionconfigs.info_value | default('INFO') }}" + when: can_run_tests | bool and read_result is succeeded + +# ==================== +# eventdefinitionconfigs – update +# ==================== + +- name: Update event definition config – set info_value to MINOR + vastdata.vms.eventdefinitionconfigs: + vms: "{{ vms_auth }}" + id: "{{ eventdefinitionconfig_id }}" + info_value: "MINOR" + state: present + register: update_result + when: can_run_tests | bool + +- name: Verify event definition config was updated + ansible.builtin.assert: + that: + - update_result is changed + - update_result.eventdefinitionconfigs is defined + - update_result.eventdefinitionconfigs.info_value == "MINOR" + when: can_run_tests | bool + +- name: Test update idempotency – run again with same params + vastdata.vms.eventdefinitionconfigs: + vms: "{{ vms_auth }}" + id: "{{ eventdefinitionconfig_id }}" + info_value: "MINOR" + state: present + register: idempotent_result + when: can_run_tests | bool + +- name: Verify update idempotency + ansible.builtin.assert: + that: + - idempotent_result is not changed + when: can_run_tests | bool + +- name: Restore original info_value + vastdata.vms.eventdefinitionconfigs: + vms: "{{ vms_auth }}" + id: "{{ eventdefinitionconfig_id }}" + info_value: "{{ saved_info_value }}" + state: present + when: can_run_tests | bool + +# ==================== +# eventdefinitionconfigs – check mode +# ==================== + +- name: Test check mode – update info_value (no actual change) + vastdata.vms.eventdefinitionconfigs: + vms: "{{ vms_auth }}" + id: "{{ eventdefinitionconfig_id }}" + info_value: "MAJOR" + state: present + register: check_mode_result + check_mode: true + when: can_run_tests | bool + +- name: Verify check mode reported change + ansible.builtin.assert: + that: + - check_mode_result is changed + when: can_run_tests | bool + +- name: Apply same update without check mode + vastdata.vms.eventdefinitionconfigs: + vms: "{{ vms_auth }}" + id: "{{ eventdefinitionconfig_id }}" + info_value: "MAJOR" + state: present + register: apply_result + when: can_run_tests | bool + +- name: Verify actual update + ansible.builtin.assert: + that: + - apply_result is changed + - apply_result.eventdefinitionconfigs.info_value == "MAJOR" + when: can_run_tests | bool + +- name: Restore info_value after check mode test + vastdata.vms.eventdefinitionconfigs: + vms: "{{ vms_auth }}" + id: "{{ eventdefinitionconfig_id }}" + info_value: "{{ saved_info_value }}" + state: present + when: can_run_tests | bool + +# ==================== +# eventdefinitionconfigs – test syslog_port idempotency +# ==================== + +- name: Save current syslog configuration for restore + ansible.builtin.set_fact: + saved_syslog_port: "{{ read_result.eventdefinitionconfigs.syslog_port | default(514) }}" + saved_syslog_protocol: "{{ read_result.eventdefinitionconfigs.syslog_protocol | default('udp') }}" + when: can_run_tests | bool and read_result is succeeded + +- name: Update syslog_port (first run) + vastdata.vms.eventdefinitionconfigs: + vms: "{{ vms_auth }}" + id: "{{ eventdefinitionconfig_id }}" + syslog_port: 520 + syslog_protocol: "tcp" + email_recipients: [] + state: present + register: syslog_update_result + when: can_run_tests | bool + +- name: Verify syslog_port was updated + ansible.builtin.assert: + that: + - syslog_update_result.eventdefinitionconfigs is defined + - syslog_update_result.eventdefinitionconfigs.syslog_port == 520 + - syslog_update_result.eventdefinitionconfigs.syslog_protocol == "tcp" + when: can_run_tests | bool + +- name: Test syslog_port idempotency – run again with same params + vastdata.vms.eventdefinitionconfigs: + vms: "{{ vms_auth }}" + id: "{{ eventdefinitionconfig_id }}" + syslog_port: 520 + syslog_protocol: "tcp" + email_recipients: [] + state: present + register: syslog_idempotent_result + when: can_run_tests | bool + +- name: Verify syslog_port idempotency (should not report changed) + ansible.builtin.assert: + that: + - syslog_idempotent_result is not changed + fail_msg: "syslog_port idempotency failed - module reported changed when no changes were made" + when: can_run_tests | bool + +- name: Restore original syslog configuration + vastdata.vms.eventdefinitionconfigs: + vms: "{{ vms_auth }}" + id: "{{ eventdefinitionconfig_id }}" + syslog_port: "{{ saved_syslog_port }}" + syslog_protocol: "{{ saved_syslog_protocol }}" + state: present + when: can_run_tests | bool + +# ==================== +# eventdefinitionconfigs – test syslog_port idempotency +# ==================== + +- name: Save current syslog configuration for restore + ansible.builtin.set_fact: + saved_syslog_port: "{{ read_result.eventdefinitionconfigs.syslog_port | default(514) }}" + saved_syslog_protocol: "{{ read_result.eventdefinitionconfigs.syslog_protocol | default('udp') }}" + when: can_run_tests | bool and read_result is succeeded + +- name: Update syslog_port (first run) + vastdata.vms.eventdefinitionconfigs: + vms: "{{ vms_auth }}" + id: "{{ eventdefinitionconfig_id }}" + syslog_port: 520 + syslog_protocol: "tcp" + email_recipients: [] + state: present + register: syslog_update_result + when: can_run_tests | bool + +- name: Verify syslog_port was updated + ansible.builtin.assert: + that: + - syslog_update_result.eventdefinitionconfigs is defined + - syslog_update_result.eventdefinitionconfigs.syslog_port == 520 + - syslog_update_result.eventdefinitionconfigs.syslog_protocol == "tcp" + when: can_run_tests | bool + +- name: Test syslog_port idempotency – run again with same params + vastdata.vms.eventdefinitionconfigs: + vms: "{{ vms_auth }}" + id: "{{ eventdefinitionconfig_id }}" + syslog_port: 520 + syslog_protocol: "tcp" + email_recipients: [] + state: present + register: syslog_idempotent_result + when: can_run_tests | bool + +- name: Verify syslog_port idempotency (should not report changed) + ansible.builtin.assert: + that: + - syslog_idempotent_result is not changed + fail_msg: "syslog_port idempotency failed - module reported changed when no changes were made" + when: can_run_tests | bool + +- name: Restore original syslog configuration + vastdata.vms.eventdefinitionconfigs: + vms: "{{ vms_auth }}" + id: "{{ eventdefinitionconfig_id }}" + syslog_port: "{{ saved_syslog_port }}" + syslog_protocol: "{{ saved_syslog_protocol }}" + state: present + when: can_run_tests | bool + +# ==================== +# eventdefinitionconfigs – non-existent resource (fail with clear message) +# ==================== + +- name: Test non-existent id returns error + vastdata.vms.eventdefinitionconfigs: + vms: "{{ vms_auth }}" + id: 999999 + info_value: "INFO" + state: present + register: create_result + failed_when: false + when: can_run_tests | bool + +- name: Verify non-existent id returns error + ansible.builtin.assert: + that: + - create_result.msg is defined + - "'405' in create_result.msg or '404' in create_result.msg or 'not allowed' in create_result.msg" + when: can_run_tests | bool + +# ==================== +# Skip message when no credentials +# ==================== + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping eventdefinitionconfigs integration tests - missing credentials. + + To enable integration tests, set the following in: + tests/integration/integration_config.yml + + VAST credentials: + vast_vms: "vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration eventdefinitionconfigs -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/globalsnapstreams/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/globalsnapstreams/defaults/main.yml new file mode 100644 index 000000000..fc54042ca --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/globalsnapstreams/defaults/main.yml @@ -0,0 +1,30 @@ +--- +# Default variables for globalsnapstreams integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +# Leave empty to skip tests gracefully +vast_vms: "" +vast_username: "" +vast_password: "" + +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true + +# Test globalsnapstream configuration +test_globalsnapstream_name: "ansible-test-globalsnapstream" + +# Test view configuration for snapshot source +test_view_path: "/ansible-test-snapstream-view" +test_view_policy_id: 1 + +# Test clone destination path +test_clone_destination_path: "/ansible-test-snapstream-clone" +test_clone_destination_path_delcheck: "/ansible-test-snapstream-clone-delcheck" + +# Test snapshot configuration +test_snapshot_name: "ansible-test-snapshot" diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/globalsnapstreams/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/globalsnapstreams/tasks/main.yml new file mode 100644 index 000000000..f85ca5c22 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/globalsnapstreams/tasks/main.yml @@ -0,0 +1,528 @@ +--- +# Integration tests for globalsnapstreams module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials }}" + +- name: Generate unique test paths with timestamp + ansible.builtin.set_fact: + test_clone_destination_path: "/ansible-test-snapstream-clone-{{ ansible_date_time.epoch }}" + test_clone_destination_path_delcheck: "/ansible-test-snapstream-clone-delcheck-{{ ansible_date_time.epoch }}" + when: can_run_tests | bool + +# ==================== +# Setup - Create Prerequisites +# ==================== + +- name: Get default tenant for testing + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "default" + state: present + register: tenant_result + when: can_run_tests | bool + failed_when: false + +- name: Set default tenant ID from result + ansible.builtin.set_fact: + test_tenant_id: "{{ tenant_result.tenants.id }}" + when: can_run_tests | bool and tenant_result.tenants is defined + +- name: Create test view for snapshot source + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + policy_id: "{{ test_view_policy_id }}" + protocols: ["NFS"] + create_dir: true + state: present + register: test_view + when: can_run_tests | bool + failed_when: false + +- name: Get view ID for snapshot creation + ansible.builtin.set_fact: + test_view_id: "{{ test_view.views.id }}" + when: test_view is not skipped and test_view.views is defined + +- name: Clean up existing test snapshot if present + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name }}" + state: absent + when: can_run_tests | bool and test_view_id is defined + failed_when: false + +- name: Wait for snapshot deletion to complete + ansible.builtin.pause: + seconds: 3 + when: can_run_tests | bool + +- name: Create test snapshot + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name }}" + path: "{{ test_view_path }}" + state: present + register: snapshot_result + when: can_run_tests | bool and test_view_id is defined + failed_when: false + +- name: Set snapshot ID for globalsnapstream tests + ansible.builtin.set_fact: + test_snapshot_id: "{{ snapshot_result.snapshots.id }}" + when: snapshot_result is not skipped and snapshot_result is not failed and snapshot_result.snapshots is defined and snapshot_result.snapshots.id is defined + +# ==================== +# Cleanup - Remove existing test globalsnapstreams if present +# ==================== + +- name: Clean up - ensure test globalsnapstreams don't exist before starting + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}" + remove_dir: true + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Create Tests +# ==================== + +- name: Test creating a globalsnapstream (local clone) + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}" + loanee_snapshot_id: "{{ test_snapshot_id }}" + loanee_root_path: "{{ test_clone_destination_path }}" + loanee_tenant_id: "{{ test_tenant_id }}" + enabled: true + state: present + wait: true + wait_timeout: 300 + register: result + when: can_run_tests | bool and test_snapshot_id is defined + +- name: Verify globalsnapstream was created + ansible.builtin.assert: + that: + - result is changed + - result.globalsnapstreams is defined + - result.globalsnapstreams.name == test_globalsnapstream_name + - result.globalsnapstreams.id is defined + - result.diff is defined + - result.diff.before == {} + - result.diff.after is defined + when: result is not skipped + +- name: Save globalsnapstream ID for later tests + ansible.builtin.set_fact: + test_globalsnapstream_id: "{{ result.globalsnapstreams.id }}" + when: result is not skipped + +# ==================== +# Idempotency Tests - Create +# ==================== + +- name: Test idempotency - create same globalsnapstream again + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}" + loanee_snapshot_id: "{{ test_snapshot_id }}" + loanee_root_path: "{{ test_clone_destination_path }}" + loanee_tenant_id: "{{ test_tenant_id }}" + enabled: true + state: present + register: result + when: can_run_tests | bool and test_globalsnapstream_id is defined + +- name: Verify idempotency (should not change) + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Lookup by ID Tests +# ==================== + +- name: Test lookup by ID + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + id: "{{ test_globalsnapstream_id }}" + state: present + register: result + when: can_run_tests | bool and test_globalsnapstream_id is defined + +- name: Verify lookup by ID works + ansible.builtin.assert: + that: + - result is not changed + - result.globalsnapstreams is defined + - result.globalsnapstreams.id == test_globalsnapstream_id + - result.globalsnapstreams.name == test_globalsnapstream_name + when: result is not skipped + +# ==================== +# Check Mode Tests - Update +# ==================== + +- name: Test check mode - lookup existing resource + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}" + state: present + check_mode: true + register: result + when: can_run_tests | bool and test_globalsnapstream_id is defined + +- name: Verify check mode lookup works + ansible.builtin.assert: + that: + - result is not changed + - result.globalsnapstreams is defined + when: result is not skipped + +# ==================== +# Check Mode Tests - Create +# ==================== + +- name: Test check mode - create new globalsnapstream + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}-checkmode" + loanee_snapshot_id: "{{ test_snapshot_id }}" + loanee_root_path: "/ansible-test-snapstream-clone-checkmode" + loanee_tenant_id: "{{ test_tenant_id }}" + enabled: true + state: present + check_mode: true + register: result + when: can_run_tests | bool and test_snapshot_id is defined + +- name: Verify check mode create reported changes + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually create the globalsnapstream + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}-checkmode" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify globalsnapstream was never created + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Update Tests - Test enabled field can be toggled +# ==================== + +- name: Test update - disable enabled on existing globalsnapstream + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}" + loanee_snapshot_id: "{{ test_snapshot_id }}" + loanee_root_path: "{{ test_clone_destination_path }}" + loanee_tenant_id: "{{ test_tenant_id }}" + enabled: false + state: present + register: result + when: can_run_tests | bool and test_globalsnapstream_id is defined + +- name: Verify enabled was updated to false + ansible.builtin.assert: + that: + - result is changed + - result.globalsnapstreams is defined + - result.globalsnapstreams.enabled == false + - result.diff is defined + - result.diff.before.enabled == true + - result.diff.after.enabled == false + when: result is not skipped + +- name: Test update idempotency - set enabled=false again + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}" + loanee_snapshot_id: "{{ test_snapshot_id }}" + loanee_root_path: "{{ test_clone_destination_path }}" + loanee_tenant_id: "{{ test_tenant_id }}" + enabled: false + state: present + register: result + when: can_run_tests | bool and test_globalsnapstream_id is defined + +- name: Verify update idempotency (should not change) + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +- name: Test update - re-enable globalsnapstream + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}" + loanee_snapshot_id: "{{ test_snapshot_id }}" + loanee_root_path: "{{ test_clone_destination_path }}" + loanee_tenant_id: "{{ test_tenant_id }}" + enabled: true + state: present + register: result + when: can_run_tests | bool and test_globalsnapstream_id is defined + +- name: Verify enabled was updated to true + ansible.builtin.assert: + that: + - result is changed + - result.globalsnapstreams is defined + - result.globalsnapstreams.enabled == true + - result.diff is defined + - result.diff.before.enabled == false + - result.diff.after.enabled == true + when: result is not skipped + +# ==================== +# Wait for Completion +# ==================== + +- name: Wait for globalsnapstream to reach Completed state + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + id: "{{ test_globalsnapstream_id }}" + state: present + register: wait_result + until: wait_result.globalsnapstreams.state == "Completed" + retries: 60 + delay: 10 + when: can_run_tests | bool and test_globalsnapstream_id is defined + +- name: Verify globalsnapstream reached Completed state + ansible.builtin.assert: + that: + - wait_result.globalsnapstreams.state == "Completed" + when: wait_result is not skipped + +# ==================== +# Delete Tests +# ==================== + +- name: Test deleting globalsnapstream + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}" + remove_dir: true + state: absent + wait: true + wait_timeout: 300 + register: result + when: can_run_tests | bool and test_globalsnapstream_id is defined + +- name: Verify globalsnapstream was deleted + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after == {} + when: result is not skipped + +# ==================== +# Idempotency Tests - Delete +# ==================== + +- name: Test delete idempotency - delete again + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Check Mode Delete Test +# ==================== + +- name: Create globalsnapstream for check mode delete test + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}-delcheck" + loanee_snapshot_id: "{{ test_snapshot_id }}" + loanee_root_path: "{{ test_clone_destination_path_delcheck }}" + loanee_tenant_id: "{{ test_tenant_id }}" + enabled: false + state: present + wait: true + register: result + when: can_run_tests | bool and test_snapshot_id is defined + +- name: Test check mode - delete + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}-delcheck" + state: absent + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported deletion + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually delete + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}-delcheck" + state: present + register: result + when: can_run_tests | bool + +- name: Check that globalsnapstream still exists + ansible.builtin.assert: + that: + - result is not changed + - result.globalsnapstreams is defined + when: result is not skipped + +- name: Clean up check mode delete test globalsnapstream + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}-delcheck" + remove_dir: true + state: absent + wait: true + when: can_run_tests | bool + failed_when: false + +# ==================== +# Error Handling Tests +# ==================== + +- name: Test error handling - neither name nor ID provided + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + loanee_snapshot_id: "{{ test_snapshot_id }}" + state: present + register: result + when: can_run_tests | bool and test_snapshot_id is defined + ignore_errors: true + +- name: Verify error was caught + ansible.builtin.assert: + that: + - result is failed + - "'name' in result.msg or 'id' in result.msg" + when: result is not skipped + +- name: Test error handling - invalid state + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}" + state: invalid + register: result + when: can_run_tests | bool + ignore_errors: true + +- name: Verify error was caught + ansible.builtin.assert: + that: + - result is failed + when: result is not skipped + +# ==================== +# Cleanup Prerequisites +# ==================== + +- name: Clean up test snapshot + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up test view + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - fallback delete local cloned destination directories as views + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ item }}" + state: absent + loop: + - "{{ test_clone_destination_path }}" + - "{{ test_clone_destination_path_delcheck }}" + - "/ansible-test-snapstream-clone-checkmode" + when: can_run_tests | bool + failed_when: false + +# ==================== +# Final Cleanup +# ==================== + +- name: Final cleanup - remove test globalsnapstream if it exists + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}" + remove_dir: true + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove delcheck test globalsnapstream if it exists + vastdata.vms.globalsnapstreams: + vms: "{{ vms_auth }}" + name: "{{ test_globalsnapstream_name }}-delcheck" + remove_dir: true + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping globalsnapstreams integration tests - credentials not configured. + + To enable integration tests, set credentials in: + tests/integration/integration_config.yml + + Example: + vast_vms: "vast-vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration globalsnapstream -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/groups/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/groups/defaults/main.yml new file mode 100644 index 000000000..f9aa90fce --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/groups/defaults/main.yml @@ -0,0 +1,21 @@ +--- +# Default variables for groups integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +# Leave empty to skip tests gracefully +vast_vms: "" +vast_username: "" +vast_password: "" + +# Test group configuration +test_group_name: ansible-test-group +test_group_gid: 8888 + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/groups/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/groups/tasks/main.yml new file mode 100644 index 000000000..73afb4ac1 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/groups/tasks/main.yml @@ -0,0 +1,531 @@ +--- +# Integration tests for groups module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials }}" + +- name: Get default tenant to obtain local_provider_id + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "default" + state: present + register: tenant_result + when: can_run_tests | bool + +- name: Set local_provider_id from default tenant + ansible.builtin.set_fact: + test_local_provider_id: "{{ tenant_result.tenants.local_provider_id }}" + when: can_run_tests | bool + +# ==================== +# Cleanup - Remove existing test group if present +# ==================== + +- name: Clean up - ensure test group doesn't exist before starting + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +# ==================== +# Create Tests +# ==================== + +- name: Test creating a group + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}" + gid: "{{ test_group_gid }}" + local_provider_id: "{{ test_local_provider_id }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify group was created + ansible.builtin.assert: + that: + - result is changed + - result.groups is defined + - result.groups.name == test_group_name + - result.groups.gid == test_group_gid + - result.diff is defined + - result.diff.before == {} + - result.diff.after is defined + when: result is not skipped + +# ==================== +# Idempotency Tests - Create +# ==================== + +- name: Test idempotency - create same group again + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}" + gid: "{{ test_group_gid }}" + local_provider_id: "{{ test_local_provider_id }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify idempotency (resource unchanged after second run) + ansible.builtin.assert: + that: + - result.groups is defined + - result.groups.name == test_group_name + - result.groups.gid == test_group_gid + when: result is not skipped + +# ==================== +# Update Tests +# ==================== + +- name: Test updating group - change name (keeping gid + local_provider_id) + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-updated" + gid: "{{ test_group_gid }}" + local_provider_id: "{{ test_local_provider_id }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify group name was updated + ansible.builtin.assert: + that: + - result is changed + - result.groups is defined + - result.groups.name == test_group_name + '-updated' + - result.groups.gid == test_group_gid + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: result is not skipped + +- name: Test update idempotency + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-updated" + gid: "{{ test_group_gid }}" + local_provider_id: "{{ test_local_provider_id }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify update idempotency (resource unchanged after second update run) + ansible.builtin.assert: + that: + - result is not changed + - result.groups is defined + - result.groups.name == test_group_name + '-updated' + - result.groups.gid == test_group_gid + when: result is not skipped + +- name: Change name back to original for subsequent tests + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}" + gid: "{{ test_group_gid }}" + local_provider_id: "{{ test_local_provider_id }}" + state: present + register: result + when: can_run_tests | bool + +# ==================== +# Check Mode Tests - Update +# ==================== + +- name: Test check mode - update name + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-checkmode-test" + gid: "{{ test_group_gid }}" + local_provider_id: "{{ test_local_provider_id }}" + state: present + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported changes + ansible.builtin.assert: + that: + - result is changed + - result.groups is defined + when: result is not skipped + +- name: Verify check mode didn't actually make changes + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}" + gid: "{{ test_group_gid }}" + local_provider_id: "{{ test_local_provider_id }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify name is still original (unchanged by check mode) + ansible.builtin.assert: + that: + - result is not changed + - result.groups.name == test_group_name + when: result is not skipped + +# ==================== +# Check Mode Tests - Create +# ==================== + +- name: Test check mode - create new group + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-checkmode" + gid: 7776 + state: present + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode create reported changes + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually create the group + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-checkmode" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify group was never created + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Delete Tests +# ==================== + +- name: Test deleting group + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify group was deleted + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after == {} + when: result is not skipped + +# ==================== +# Idempotency Tests - Delete +# ==================== + +- name: Test delete idempotency - delete again + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Check Mode Delete Test +# ==================== + +- name: Create group for check mode delete test + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-delcheck" + gid: 7775 + local_provider_id: "{{ test_local_provider_id }}" + state: present + register: result + when: can_run_tests | bool + +- name: Test check mode - delete + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-delcheck" + state: absent + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported deletion + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually delete + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-delcheck" + state: present + register: result + when: can_run_tests | bool + +- name: Check that group still exists + ansible.builtin.assert: + that: + - result is not changed + - result.groups is defined + when: result is not skipped + +- name: Clean up check mode delete test group + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-delcheck" + state: absent + when: can_run_tests | bool + +# ==================== +# Error Handling Tests +# ==================== + +- name: Test error handling - invalid state + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}" + state: invalid + register: result + when: can_run_tests | bool + ignore_errors: true + +- name: Verify error was caught + ansible.builtin.assert: + that: + - result is failed + when: result is not skipped + +# ==================== +# Idempotency local_provider_id test +# ==================== + +- name: Clean up - ensure local_provider_id idempotency test group doesn't exist + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-lp-idempotency" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Create local provider for local_provider_id idempotency test + vastdata.vms.localproviders: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-lp" + state: present + register: lp_idempotency + when: can_run_tests | bool + +- name: Set local_provider_id from created local provider + ansible.builtin.set_fact: + lp_idempotency_provider_id: "{{ lp_idempotency.localproviders.id }}" + when: can_run_tests | bool + +- name: Create group with local_provider_id (first run - should change) + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-lp-idempotency" + gid: 99902 + local_provider_id: "{{ lp_idempotency_provider_id }}" + state: present + register: lp_idempotency_result + when: can_run_tests | bool + +- name: Verify group was created (changed=1) + ansible.builtin.assert: + that: + - lp_idempotency_result is changed + - lp_idempotency_result.groups is defined + - lp_idempotency_result.groups.name == test_group_name + '-lp-idempotency' + when: lp_idempotency_result is not skipped + +- name: Run same create again - idempotency (second run - should NOT change) + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-lp-idempotency" + gid: 99902 + local_provider_id: "{{ lp_idempotency_provider_id }}" + state: present + register: lp_idempotency_result2 + when: can_run_tests | bool + +- name: Verify idempotency - second run must report changed=0 + ansible.builtin.assert: + that: + - not lp_idempotency_result2.changed + fail_msg: "Second run with local_provider_id incorrectly reported changed=1. API may not return local_provider in GET response." + when: lp_idempotency_result2 is not skipped + +- name: Clean up local_provider_id idempotency test group + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-lp-idempotency" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up local provider from idempotency test + vastdata.vms.localproviders: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-lp" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up - ensure rename test group doesn't exist + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-rename-original" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up - ensure renamed test group doesn't exist + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-rename-new" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Create group for rename test + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-rename-original" + gid: 12345 + local_provider_id: "{{ test_local_provider_id }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify group was created + ansible.builtin.assert: + that: + - result is changed + - result.groups is defined + - result.groups.name == test_group_name + '-rename-original' + - result.groups.gid == 12345 + when: result is not skipped + +- name: Rename group by changing name while keeping gid and local_provider_id + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-rename-new" + gid: 12345 + local_provider_id: "{{ test_local_provider_id }}" + state: present + register: rename_result + when: can_run_tests | bool + +- name: Verify group was renamed (not created as new) + ansible.builtin.assert: + that: + - rename_result is changed + - rename_result.groups is defined + - rename_result.groups.name == test_group_name + '-rename-new' + - rename_result.groups.gid == 12345 + - rename_result.diff is defined + - rename_result.diff.before.name == test_group_name + '-rename-original' + - rename_result.diff.after.name == test_group_name + '-rename-new' + fail_msg: "Cannot rename group. Module should update existing group by gid+local_provider_id, not create new one." + when: rename_result is not skipped + +- name: Verify old group name no longer exists + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-rename-original" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify old name is gone (no change should be needed) + ansible.builtin.assert: + that: + - result is not changed + fail_msg: "Old group name still exists, indicating a new group was created instead of renaming." + when: result is not skipped + +- name: Test rename idempotency - run same rename again + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-rename-new" + gid: 12345 + local_provider_id: "{{ test_local_provider_id }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify rename idempotency + ansible.builtin.assert: + that: + - result is not changed + - result.groups.name == test_group_name + '-rename-new' + when: result is not skipped + +- name: Clean up renamed group + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}-rename-new" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Cleanup +# ==================== + +- name: Final cleanup - remove test group if it exists + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_group_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping groups integration tests - credentials not configured. + + To enable integration tests, set credentials in: + tests/integration/integration_config.yml + + Example: + vast_vms: "vast-vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration groups -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/ldaps/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/ldaps/defaults/main.yml new file mode 100644 index 000000000..4b82d0d9d --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/ldaps/defaults/main.yml @@ -0,0 +1,35 @@ +--- +# Default variables for ldaps integration tests +# +# LDAP configuration is loaded dynamically from sites.yaml based on detected region. +# See: docs/SITE_CONFIG.md and docs/REGION_DETECTION.md +# +# To run tests: +# 1. Auto (using sites.yaml): VAST_SITE=kfs ./test.sh integration ldaps +# 2. Manual: Set environment variables and run ./test.sh integration ldaps +# 3. Legacy: Override these in tests/integration/integration_config.yml + +# VAST system connection details +# Leave empty to skip tests gracefully +vast_vms: "" +vast_username: "" +vast_password: "" + +# Test LDAP configuration +# These are loaded from sites.yaml via environment variables +# If not set, tests will be skipped +test_ldap_domain: "" +test_ldap_searchbase: "" +test_ldap_binddn: "" +test_ldap_bindpw: "" +test_ldap_urls: [] +test_ldap_port: 389 +test_ldap_method: "simple" + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/ldaps/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/ldaps/tasks/main.yml new file mode 100644 index 000000000..8647a2f40 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/ldaps/tasks/main.yml @@ -0,0 +1,511 @@ +--- +# Integration tests for ldaps module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml +# Requires: LDAP server configured and accessible from VAST system + +# ==================== +# Setup +# ==================== + +- name: Check if VAST credentials are available + ansible.builtin.set_fact: + has_vast_credentials: >- + {{ + (vast_vms | default('') | length > 0) and + (vast_username | default('') | length > 0) and + (vast_password | default('') | length > 0) + }} + +- name: Check if LDAP configuration is available + ansible.builtin.set_fact: + has_ldap_config: >- + {{ + (test_ldap_domain | default('') | length > 0) and + (test_ldap_urls | default([]) | length > 0) + }} + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_vast_credentials and has_ldap_config }}" + +- name: Display test configuration + ansible.builtin.debug: + msg: | + ╔════════════════════════════════════════════════════════════════╗ + ║ Test Configuration ║ + ╚════════════════════════════════════════════════════════════════╝ + + VAST Connection: + VMS: {{ vast_vms | default('not set') }} + Username: {{ vast_username | default('not set') }} + Password: {{ '***set***' if vast_password else 'not set' }} + + LDAP Configuration: + Domain: {{ test_ldap_domain | default('not set') }} + URLs: {{ test_ldap_urls | default([]) | join(', ') }} + Type: {{ test_ldap_type | default('not set') }} + Use TLS: {{ test_ldap_use_tls | default('not set') }} + Port: {{ test_ldap_port | default(389) }} + Method: {{ test_ldap_method | default('simple') }} + Search Base: {{ test_ldap_searchbase | default('not set') }} + Group Search: {{ test_ldap_group_searchbase | default('not set') }} + Bind DN: {{ test_ldap_binddn | default('not set') }} + Bind Password: {{ '***set***' if test_ldap_bindpw else 'not set' }} + when: can_run_tests + +# ==================== +# Cleanup - Remove existing test LDAP if present +# ==================== + +- name: Clean up - ensure test LDAP doesn't exist before starting + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-{{ test_ldap_domain }}" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +# ==================== +# Create Tests +# ==================== + +- name: Test creating LDAP configuration + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-{{ test_ldap_domain }}" + domain_name: "{{ test_ldap_domain }}" + urls: "{{ test_ldap_urls }}" + searchbase: "{{ test_ldap_searchbase }}" + binddn: "{{ test_ldap_binddn }}" + bindpw: "{{ test_ldap_bindpw }}" + port: "{{ test_ldap_port }}" + method: "{{ test_ldap_method }}" + use_tls: "{{ test_ldap_use_tls | default(false) }}" + use_ldaps: false + state: present + register: result + when: can_run_tests | bool + +# Pass when we just created (changed) or when LDAP already existed (e.g. clean-up could not delete due to tenants attached). +- name: Verify LDAP was created + ansible.builtin.assert: + that: + - result.ldaps is defined + - result.ldaps.id is defined + - result.ldaps.name is defined + - result.ldaps.port == test_ldap_port | int + - result.ldaps.method == test_ldap_method + - (result is changed and result.diff is defined and result.diff.before == {} and result.diff.after is defined) or (result is not changed) + fail_msg: "LDAP creation verification failed" + success_msg: "LDAP configuration created and verified successfully" + when: result is not skipped + # Note: Cannot verify exact name value due to API masking sensitive fields + +# ==================== +# Idempotency Tests - Create +# ==================== + +- name: Test idempotency - create same LDAP again + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-{{ test_ldap_domain }}" + domain_name: "{{ test_ldap_domain }}" + urls: "{{ test_ldap_urls }}" + searchbase: "{{ test_ldap_searchbase }}" + binddn: "{{ test_ldap_binddn }}" + bindpw: "{{ test_ldap_bindpw }}" + port: "{{ test_ldap_port }}" + method: "{{ test_ldap_method }}" + use_tls: "{{ test_ldap_use_tls | default(false) }}" + use_ldaps: false + state: present + register: result + when: can_run_tests | bool + +- name: Verify idempotency (should not change) + ansible.builtin.assert: + that: + - result is not changed + - result.ldaps is defined + when: result is not skipped + +# ==================== +# Update Tests +# ==================== + +- name: Test updating LDAP - change monitor action + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-{{ test_ldap_domain }}" + monitor_action: "BIND" + state: present + register: result + when: can_run_tests | bool + +- name: Verify LDAP was updated + ansible.builtin.assert: + that: + - result is changed + - result.ldaps is defined + - result.ldaps.monitor_action == "BIND" + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: result is not skipped + +- name: Test update idempotency + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-{{ test_ldap_domain }}" + monitor_action: "BIND" + state: present + register: result + when: can_run_tests | bool + +- name: Verify update idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Update Authentication Settings +# ==================== + +- name: Test updating LDAP - change method + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-{{ test_ldap_domain }}" + method: "anonymous" + state: present + register: result + when: can_run_tests | bool + +- name: Verify authentication method was updated + ansible.builtin.assert: + that: + - result is changed + - result.ldaps is defined + - result.ldaps.method == "anonymous" + when: result is not skipped + +# ==================== +# Check Mode Tests +# ==================== + +- name: Test check mode - change monitor action back to PING + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-{{ test_ldap_domain }}" + monitor_action: "PING" + state: present + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported changes + ansible.builtin.assert: + that: + - result is changed + - result.ldaps is defined + when: result is not skipped + +- name: Verify check mode didn't actually make changes + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-{{ test_ldap_domain }}" + state: present + register: result + when: can_run_tests | bool + +- name: Check that monitor_action is still BIND (not PING) + ansible.builtin.assert: + that: + - result.ldaps.monitor_action == "BIND" + when: result is not skipped + +# ==================== +# VMS Authentication Tests +# ==================== + +- name: Test enabling VMS authentication + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-{{ test_ldap_domain }}" + is_vms_auth_provider: true + state: present + register: enable_vms_result + when: can_run_tests | bool + +- name: Verify VMS authentication was enabled + ansible.builtin.assert: + that: + - enable_vms_result is changed + - enable_vms_result.ldaps is defined + - enable_vms_result.ldaps.is_vms_auth_provider == true + when: enable_vms_result is not skipped + +# ==================== +# Set POSIX Primary Tests (via dedicated ldap_set_posix_primary module) +# ==================== + +- name: Lookup LDAP ID for set_posix_primary test + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-{{ test_ldap_domain }}" + state: present + register: ldap_lookup + when: can_run_tests | bool + +- name: Test set_posix_primary via dedicated module (check mode) + vastdata.vms.ldap_set_posix_primary: + vms: "{{ vms_auth }}" + ldap_id: "{{ ldap_lookup.ldaps.id }}" + check_mode: true + register: result_check_posix + when: can_run_tests | bool and ldap_lookup is not skipped + failed_when: false + +- name: Verify set_posix_primary check mode reported change or multi-tenant skip + ansible.builtin.assert: + that: + - (result_check_posix is changed) or ('multiple tenants' in (result_check_posix.msg | default('') | lower)) + fail_msg: "Check mode for set_posix_primary failed unexpectedly (not the known multi-tenant 400)" + when: result_check_posix is not skipped + +# ==================== +# Multi-tenant: POSIX primary via tenants API (happy path when ldaps set_posix_primary returns 400) +# ==================== +- name: Set fact when cluster is multi-tenant (ldap_set_posix_primary returned 400) + ansible.builtin.set_fact: + ldap_posix_multi_tenant: "{{ 'multiple tenants' in (result_check_posix.msg | default('') | lower) }}" + when: result_check_posix is not skipped + +# ==================== +# Check Mode Delete Test +# ==================== + +- name: Test check mode - delete (should report change but not actually delete) + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-{{ test_ldap_domain }}" + state: absent + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported deletion would happen + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually delete (LDAP should still exist) + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-{{ test_ldap_domain }}" + state: present + register: result + when: can_run_tests | bool + +- name: Check that LDAP still exists after check mode + ansible.builtin.assert: + that: + - result is not changed + - result.ldaps is defined + - result.ldaps.name is defined + when: result is not skipped + +# ==================== +# Delete Tests +# ==================== + +- name: Test deleting LDAP configuration + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-{{ test_ldap_domain }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify LDAP was deleted + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after == {} + when: result is not skipped + +# ==================== +# Idempotency Tests - Delete +# ==================== + +- name: Test delete idempotency - delete again + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-{{ test_ldap_domain }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Cleanup +# ==================== + +- name: Final cleanup - remove test LDAP if it exists + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-{{ test_ldap_domain }}" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Test name parameter without domain_name +# ==================== + +- name: Clean up - ensure test LDAP by name doesn't exist + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-name-only" + state: absent + register: cleanup_name_result + when: can_run_tests | bool + failed_when: false + +- name: Create LDAP with name parameter (without domain_name) + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-name-only" + urls: ["ldap://10.27.252.30"] + binddn: "cn=admin,dc=qa,dc=vastdata,dc=com" + searchbase: "dc=qa,dc=vastdata,dc=com" + bindpw: "vastdata" + port: 389 + method: "simple" + state: present + register: create_name_result + when: can_run_tests | bool + +- name: Verify LDAP was created with name + ansible.builtin.assert: + that: + - create_name_result is changed + - create_name_result.ldaps is defined + - create_name_result.ldaps.id is defined + - create_name_result.diff is defined + - create_name_result.diff.before == {} + - create_name_result.diff.after is defined + fail_msg: "LDAP creation with name parameter failed" + success_msg: "LDAP configuration created with name successfully" + when: create_name_result is not skipped + +- name: Test idempotency - create same LDAP by name again + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-name-only" + urls: ["ldap://10.27.252.30"] + binddn: "cn=admin,dc=qa,dc=vastdata,dc=com" + searchbase: "dc=qa,dc=vastdata,dc=com" + bindpw: "vastdata" + port: 389 + method: "simple" + state: present + register: idempotency_name_result + when: can_run_tests | bool + +- name: Verify idempotency with name (should not report changed) + ansible.builtin.assert: + that: + - idempotency_name_result is not changed + - idempotency_name_result.ldaps is defined + fail_msg: "name breaks idempotency - ldap reported as changed when it shouldn't be" + when: idempotency_name_result is not skipped + +- name: Update LDAP by name - change monitor action + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-name-only" + monitor_action: "BIND" + state: present + register: update_name_result + when: can_run_tests | bool + +- name: Verify LDAP was updated by name + ansible.builtin.assert: + that: + - update_name_result is changed + - update_name_result.ldaps is defined + - update_name_result.ldaps.monitor_action == "BIND" + - update_name_result.diff is defined + when: update_name_result is not skipped + +- name: Delete LDAP by name + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-name-only" + state: absent + register: delete_name_result + when: can_run_tests | bool + +- name: Verify LDAP was deleted by name + ansible.builtin.assert: + that: + - delete_name_result is changed + - delete_name_result.diff is defined + - delete_name_result.diff.before is defined + - delete_name_result.diff.after == {} + when: delete_name_result is not skipped + +- name: Test delete idempotency by name + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-name-only" + state: absent + register: delete_idempotency_name_result + when: can_run_tests | bool + +- name: Verify delete idempotency by name + ansible.builtin.assert: + that: + - delete_idempotency_name_result is not changed + when: delete_idempotency_name_result is not skipped + +# ==================== +# Final Cleanup +# ==================== + +- name: Final cleanup - remove test LDAP by name if it exists + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "test-ldap-name-only" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping ldaps integration tests - credentials not configured. + + To enable integration tests, set credentials in: + tests/integration/integration_config.yml + + Example: + vast_vms: "vast-vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration ldaps -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nativereplicationremotetargets/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nativereplicationremotetargets/defaults/main.yml new file mode 100644 index 000000000..dcc607dfd --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nativereplicationremotetargets/defaults/main.yml @@ -0,0 +1,37 @@ +--- +# Default variables for nativereplicationremotetargets integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +# Leave empty to skip tests gracefully +vast_vms: "" +vast_username: "" +vast_password: "" +vast_vms_2: "" + +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true + +# Native replication remote target test configuration +test_nativereplication_name: "ansible-test-replication-target" +test_nativereplication_renamed: "ansible-test-replication-renamed" + +# Test REPLICATION VIP pool configuration (created if none exists) +test_replication_vippool_name: "ansible-test-replication-vippool" +test_replication_vippool_subnet_cidr: 16 +test_replication_vippool_ip_ranges: + - ["18.18.0.100", "18.18.0.110"] + +# Test REPLICATION VIP pool configuration (created if none exists) on cluster 2 +test_replication_vippool_name_cluster2: "ansible-test-replication-vippool-cluster2" +test_replication_vippool_subnet_cidr_cluster2: 16 +test_replication_vippool_ip_ranges_cluster2: + - ["18.18.0.120", "18.18.0.130"] + +# Replication settings +test_nativereplication_secure_mode: "NONE" +test_nativereplication_transport_mode: "TCP" diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nativereplicationremotetargets/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nativereplicationremotetargets/tasks/main.yml new file mode 100644 index 000000000..c6d872b6c --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nativereplicationremotetargets/tasks/main.yml @@ -0,0 +1,550 @@ +--- +# Integration tests for nativereplicationremotetargets module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials }}" + can_run_module_tests: "{{ has_credentials | bool and (test_leading_vip | default('') | length > 0) }}" + +# ==================== +# Setup - Discover local replication VIP pool +# ==================== + +- name: Get list of all VIP pools + ansible.builtin.uri: + url: "https://{{ vast_vms }}/api/latest/vippools/" + method: GET + user: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + force_basic_auth: true + register: all_vippools + when: can_run_tests | bool + failed_when: false + +- name: Find existing replication VIP pools + ansible.builtin.set_fact: + existing_repl_pools: "{{ all_vippools.json | default([]) | selectattr('role', 'equalto', 'REPLICATION') | list }}" + when: can_run_tests | bool + +- name: Create REPLICATION VIP pool if none exists + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_replication_vippool_name }}" + role: "REPLICATION" + enabled: true + subnet_cidr: "{{ test_replication_vippool_subnet_cidr }}" + ip_ranges: "{{ test_replication_vippool_ip_ranges }}" + state: present + register: created_vippool + when: can_run_tests | bool and existing_repl_pools | default([]) | length == 0 + +- name: Set flag to track if we created the VIP pool + ansible.builtin.set_fact: + we_created_replication_vippool: true + when: created_vippool is not skipped and created_vippool is changed + +- name: Set local pool ID for tests (from existing pool) + ansible.builtin.set_fact: + test_pool_id: "{{ existing_repl_pools[0].id }}" + when: can_run_tests | bool and existing_repl_pools | default([]) | length > 0 + +- name: Set local pool ID for tests (from newly created pool) + ansible.builtin.set_fact: + test_pool_id: "{{ created_vippool.vippools.id }}" + when: created_vippool is not skipped and created_vippool is changed + +- name: Report which replication VIP pool will be used + ansible.builtin.debug: + msg: "Using replication VIP pool ID {{ test_pool_id }} ({{ 'created' if we_created_replication_vippool | default(false) else 'existing' }})" + when: can_run_tests | bool and test_pool_id is defined + +# ==================== +# Setup - leading VIP from cluster 2 +# ==================== + +- name: Check if second cluster is available + ansible.builtin.set_fact: + has_cluster_2: "{{ vast_vms_2 | default('') | length > 0 }}" + when: can_run_tests | bool + +- name: Parse cluster 2 connection details + ansible.builtin.set_fact: + cluster2_port: "{{ vast_vms_2.split(':')[1] if ':' in vast_vms_2 else '443' }}" + cluster2_needs_tunnel: "{{ ':' in vast_vms_2 and vast_vms_2.split(':')[1] != '443' }}" + when: can_run_tests | bool and has_cluster_2 | bool + +- name: Establish SSH tunnel to cluster 2 + ansible.builtin.raw: | + ssh -f -N -i /vast/deploy/ssh_key.pem \ + -L 0.0.0.0:{{ cluster2_port }}:16.0.0.2:443 \ + centos@localhost + delegate_to: "{{vast_vms}}" + vars: + ansible_user: centos + when: can_run_tests | bool and has_cluster_2 | bool and cluster2_needs_tunnel | bool + register: ssh_tunnel + failed_when: false + changed_when: false + +- name: Wait for tunnel to be ready + ansible.builtin.wait_for: + host: "{{ vast_vms }}" + port: "{{ cluster2_port }}" + timeout: 10 + when: can_run_tests | bool and has_cluster_2 | bool and cluster2_needs_tunnel | bool + +- name: Check cluster 2 is reachable and get VIP pools + ansible.builtin.uri: + url: "https://{{ vast_vms_2 }}/api/latest/vippools/" + method: GET + user: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + force_basic_auth: true + register: cluster2_vippools + when: can_run_tests | bool and has_cluster_2 | bool and test_leading_vip | default('') | length == 0 + failed_when: (cluster2_vippools is not skipped) and (cluster2_vippools is failed) + +- name: Find existing replication VIP pools on cluster 2 + ansible.builtin.set_fact: + existing_repl_pools_2: "{{ cluster2_vippools.json | default([]) | selectattr('role', 'equalto', 'REPLICATION') | list }}" + when: can_run_tests | bool and has_cluster_2 | bool and test_leading_vip | default('') | length == 0 + +- name: Use first IP from existing cluster 2 VIP pool as leading VIP + ansible.builtin.set_fact: + test_leading_vip: "{{ existing_repl_pools_2[0].ip_ranges[0][0] }}" + when: >- + can_run_tests | bool and has_cluster_2 | bool and test_leading_vip | default('') | length == 0 + and existing_repl_pools_2 | default([]) | length > 0 + +- name: Create REPLICATION VIP pool on cluster 2 if none exists + vastdata.vms.vippools: + vms: + host: "{{ vast_vms_2 }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + state: present + name: "{{ test_replication_vippool_name_cluster2 }}" + role: "REPLICATION" + subnet_cidr: "{{ test_replication_vippool_subnet_cidr_cluster2 }}" + ip_ranges: "{{ test_replication_vippool_ip_ranges_cluster2 }}" + register: replication_vippool_cluster2 + when: >- + can_run_tests | bool and has_cluster_2 | bool and test_leading_vip | default('') | length == 0 + and existing_repl_pools_2 | default([]) | length == 0 + failed_when: false + changed_when: false + no_log: true + +- name: Set flag to track if we created the VIP pool on cluster 2 + ansible.builtin.set_fact: + we_created_replication_vippool_cluster2: true + when: >- + can_run_tests | bool and has_cluster_2 | bool and test_leading_vip | default('') | length == 0 + and existing_repl_pools_2 | default([]) | length == 0 + and replication_vippool_cluster2 is not skipped + and replication_vippool_cluster2 is not failed + +- name: Use first IP from newly created cluster 2 VIP pool as leading VIP + ansible.builtin.set_fact: + test_leading_vip: "{{ replication_vippool_cluster2.vippools.ip_ranges[0][0] }}" + when: >- + can_run_tests | bool and has_cluster_2 | bool and test_leading_vip | default('') | length == 0 + and we_created_replication_vippool_cluster2 | default(false) + +- name: Re-evaluate module test conditions after leading VIP discovery + ansible.builtin.set_fact: + can_run_module_tests: "{{ can_run_tests | bool and test_leading_vip | default('') | length > 0 }}" + when: can_run_tests | bool + +- name: Report discovered leading VIP + ansible.builtin.debug: + msg: "Auto-discovered leading VIP from cluster 2: {{ test_leading_vip }}" + when: can_run_tests | bool and has_cluster_2 | default(false) | bool and test_leading_vip | default('') | length > 0 + +- name: Fail if no leading_vip available + ansible.builtin.fail: + msg: >- + No leading_vip available. Set test_leading_vip in integration_config.yml + or set vast_vms_2 to auto-discover from cluster 2. + when: can_run_tests | bool and not (can_run_module_tests | bool) + +# ==================== +# Cleanup - Remove existing test replication targets if present +# ==================== + +- name: Clean up - ensure test replication target doesn't exist before starting + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + name: "{{ test_nativereplication_name }}" + state: absent + when: can_run_module_tests | bool + failed_when: false + +- name: Clean up - ensure renamed test replication target doesn't exist + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + name: "{{ test_nativereplication_renamed }}" + state: absent + when: can_run_module_tests | bool + failed_when: false + +- name: Wait for cluster to finish removing replication targets before tests start + ansible.builtin.uri: + url: "https://{{ vast_vms }}/api/latest/nativereplicationremotetargets/" + method: GET + user: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + force_basic_auth: true + status_code: [200] + register: wait_cleanup + until: >- + wait_cleanup.json | default([]) | selectattr('name', 'in', [ + test_nativereplication_name, + test_nativereplication_renamed + ]) | list | length == 0 + retries: 24 + delay: 5 + when: can_run_module_tests | bool + +# ==================== +# Create Tests +# ==================== + +- name: Test creating a native replication remote target + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + name: "{{ test_nativereplication_name }}" + leading_vip: "{{ test_leading_vip | default('') }}" + pool_id: "{{ test_pool_id }}" + secure_mode: "{{ test_nativereplication_secure_mode }}" + transport_mode: "{{ test_nativereplication_transport_mode }}" + state: present + register: result + when: can_run_module_tests | bool + +- name: Verify replication target was created + ansible.builtin.assert: + that: + - result is changed + - result.nativereplicationremotetargets is defined + - result.nativereplicationremotetargets.name == test_nativereplication_name + - result.nativereplicationremotetargets.id is defined + - result.diff is defined + - result.diff.before == {} + - result.diff.after is defined + when: result is not skipped + +- name: Save replication target ID for later tests + ansible.builtin.set_fact: + test_nativereplication_id: "{{ result.nativereplicationremotetargets.id }}" + when: result is not skipped + +# ==================== +# Idempotency Tests - Create +# ==================== + +- name: Test idempotency - create same replication target again + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + name: "{{ test_nativereplication_name }}" + leading_vip: "{{ test_leading_vip | default('') }}" + pool_id: "{{ test_pool_id }}" + secure_mode: "{{ test_nativereplication_secure_mode }}" + transport_mode: "{{ test_nativereplication_transport_mode }}" + state: present + register: result + when: can_run_module_tests | bool + +- name: Verify idempotency (should not change) + ansible.builtin.assert: + that: + - result is not changed + - result.nativereplicationremotetargets is defined + - result.nativereplicationremotetargets.id == test_nativereplication_id + when: result is not skipped + +# ==================== +# Update Tests +# ==================== + +- name: Test updating replication target - change mss + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + name: "{{ test_nativereplication_name }}" + mss: 1300 + state: present + register: result + when: can_run_module_tests | bool + +- name: Verify replication target was updated + ansible.builtin.assert: + that: + - result is changed + - result.nativereplicationremotetargets is defined + - result.nativereplicationremotetargets.id == test_nativereplication_id + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: result is not skipped + +- name: Test update idempotency + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + name: "{{ test_nativereplication_name }}" + mss: 1300 + state: present + register: result + when: can_run_module_tests | bool + +- name: Verify update idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Rename Tests +# ==================== + +- name: Test renaming replication target using ID parameter + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + id: "{{ test_nativereplication_id }}" + name: "{{ test_nativereplication_renamed }}" + state: present + register: result + when: can_run_module_tests | bool + +- name: Verify replication target was renamed + ansible.builtin.assert: + that: + - result is changed + - result.nativereplicationremotetargets is defined + - result.nativereplicationremotetargets.name == test_nativereplication_renamed + - result.nativereplicationremotetargets.id == test_nativereplication_id + - result.diff is defined + - result.diff.before.name == test_nativereplication_name + - result.diff.after.name == test_nativereplication_renamed + when: result is not skipped + +- name: Test rename idempotency + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + id: "{{ test_nativereplication_id }}" + name: "{{ test_nativereplication_renamed }}" + state: present + register: result + when: can_run_module_tests | bool + +- name: Verify rename idempotency + ansible.builtin.assert: + that: + - result is not changed + - result.nativereplicationremotetargets.name == test_nativereplication_renamed + when: result is not skipped + +- name: Test lookup by new name after rename + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + name: "{{ test_nativereplication_renamed }}" + state: present + register: result + when: can_run_module_tests | bool + +- name: Verify lookup by new name works + ansible.builtin.assert: + that: + - result is not changed + - result.nativereplicationremotetargets is defined + - result.nativereplicationremotetargets.id == test_nativereplication_id + - result.nativereplicationremotetargets.name == test_nativereplication_renamed + when: result is not skipped + +# ==================== +# Check Mode Tests - Update +# ==================== + +- name: Test check mode - update mss + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + name: "{{ test_nativereplication_renamed }}" + mss: 1400 + state: present + check_mode: true + register: result + when: can_run_module_tests | bool + +- name: Verify check mode reported changes + ansible.builtin.assert: + that: + - result is changed + - result.nativereplicationremotetargets is defined + when: result is not skipped + +- name: Verify check mode didn't actually make changes + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + name: "{{ test_nativereplication_renamed }}" + mss: 1300 + state: present + register: result + when: can_run_module_tests | bool + +- name: Verify mss unchanged by check mode + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Diff Mode Tests +# ==================== + +- name: Update replication target with diff mode + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + name: "{{ test_nativereplication_renamed }}" + mss: 1400 + state: present + diff: true + register: result + when: can_run_module_tests | bool + +- name: Verify update diff output + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: result is not skipped + +# ==================== +# Delete Tests +# ==================== + +- name: Test deleting replication target + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + name: "{{ test_nativereplication_renamed }}" + state: absent + register: result + when: can_run_module_tests | bool + +- name: Verify replication target was deleted + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after == {} + when: result is not skipped + +# ==================== +# Error Handling Tests +# ==================== + +- name: Test error handling - neither name nor ID provided + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + state: present + register: error_result + when: can_run_tests | bool + ignore_errors: true + +- name: Verify error was caught + ansible.builtin.assert: + that: + - error_result is failed + - "'name' in error_result.msg or 'id' in error_result.msg" + when: error_result is not skipped + +# ==================== +# Final Cleanup +# ==================== +- name: Delete replication VIP pool from cluster 2 if we created it + vastdata.vms.vippools: + vms: + host: "{{ vast_vms_2 }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + state: absent + name: ansible-test-replication-vippool-cluster2 + when: >- + can_run_tests | bool and has_cluster_2 | bool + and we_created_replication_vippool_cluster2 | default(false) + failed_when: false + changed_when: false + no_log: true + +- name: Final cleanup - remove test replication target if it exists + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + name: "{{ test_nativereplication_name }}" + state: absent + when: can_run_module_tests | bool + failed_when: false + +- name: Final cleanup - remove renamed test replication target if it exists + vastdata.vms.nativereplicationremotetargets: + vms: "{{ vms_auth }}" + name: "{{ test_nativereplication_renamed }}" + state: absent + when: can_run_module_tests | bool + failed_when: false + +- name: Final cleanup - remove test REPLICATION VIP pool if we created it + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_replication_vippool_name }}" + state: absent + when: can_run_tests | bool and we_created_replication_vippool | default(false) | bool + failed_when: false + +# ==================== +# Cleanup - Close SSH tunnel +# ==================== + +- name: Close SSH tunnel to cluster 2 + ansible.builtin.shell: | + pkill -f "ssh.*-L.*{{ cluster2_port }}:16.0.0.2:443" + when: can_run_tests | bool and has_cluster_2 | bool and cluster2_needs_tunnel | bool + failed_when: false + changed_when: false + +# ==================== +# Skip Message +# ==================== + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping nativereplicationremotetargets integration tests - credentials not configured. + + To enable integration tests, set the following in: + tests/integration/integration_config.yml + + VAST credentials: + vast_vms: "vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration nativereplicationremotetargets -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nonlocal_group/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nonlocal_group/defaults/main.yml new file mode 100644 index 000000000..97bb29935 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nonlocal_group/defaults/main.yml @@ -0,0 +1,30 @@ +--- +# Default variables for nonlocalgroups integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +# Leave empty to skip tests gracefully +vast_vms: "" +vast_username: "" +vast_password: "" + +# LDAP configuration (required for nonlocal group queries) +test_ldap_urls: [] +test_ldap_port: 389 +test_ldap_method: "simple" +test_ldap_use_tls: false + +# Test nonlocal group to query via LDAP +# Must be a valid group that exists in the configured LDAP directory +test_nonlocal_groupname: "vast_ansible_group" +test_nonlocal_gid: 4288448013 +# A known LDAP user to add as a group member so VAST can discover the group +test_nonlocal_username: "vast_ansible_user" + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nonlocal_group/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nonlocal_group/tasks/main.yml new file mode 100644 index 000000000..7ba1cb7fd --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nonlocal_group/tasks/main.yml @@ -0,0 +1,445 @@ +--- +# Integration tests for nonlocal_group module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml +# Requires: LDAP server configured and accessible from VAST system + +# ==================== +# Setup +# ==================== + +- name: Check if VAST credentials are available + ansible.builtin.set_fact: + has_vast_credentials: >- + {{ + (vast_vms | default('') | length > 0) and + (vast_username | default('') | length > 0) and + (vast_password | default('') | length > 0) + }} + +- name: Check if LDAP configuration is available + ansible.builtin.set_fact: + has_ldap_config: >- + {{ + (test_ldap_domain | default('') | length > 0) and + (test_ldap_urls | default([]) | length > 0) + }} + +- name: Check if a test nonlocal group is configured + ansible.builtin.set_fact: + has_nonlocal_group: "{{ test_nonlocal_groupname | default('') | length > 0 }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_vast_credentials and has_ldap_config }}" + can_run_group_tests: "{{ has_vast_credentials and has_ldap_config and has_nonlocal_group }}" + +- name: Display test configuration + ansible.builtin.debug: + msg: | + ╔════════════════════════════════════════════════════════════════╗ + ║ Test Configuration ║ + ╚════════════════════════════════════════════════════════════════╝ + + VAST Connection: + VMS: {{ vast_vms | default('not set') }} + Username: {{ vast_username | default('not set') }} + Password: {{ '***set***' if vast_password else 'not set' }} + + LDAP Configuration: + Domain: {{ test_ldap_domain | default('not set') }} + URLs: {{ test_ldap_urls | default([]) | join(', ') }} + Port: {{ test_ldap_port | default(389) }} + Method: {{ test_ldap_method | default('simple') }} + Use TLS: {{ test_ldap_use_tls | default(false) }} + Search Base: {{ test_ldap_searchbase | default('not set') }} + Bind DN: {{ test_ldap_binddn | default('not set') }} + Bind Password: {{ '***set***' if test_ldap_bindpw else 'not set' }} + + Nonlocal Group Test: + Groupname: {{ test_nonlocal_groupname | default('not set') }} + GID: {{ test_nonlocal_gid | default('not set') }} + when: can_run_tests + +# ==================== +# LDAP Setup +# ==================== + +- name: Ensure LDAP is configured (create if not exists, use if already configured) + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + domain_name: "{{ test_ldap_domain }}" + name: "{{ test_ldap_domain }}" + urls: "{{ test_ldap_urls }}" + searchbase: "{{ test_ldap_searchbase }}" + binddn: "{{ test_ldap_binddn }}" + bindpw: "{{ test_ldap_bindpw }}" + port: "{{ test_ldap_port }}" + method: "{{ test_ldap_method }}" + use_tls: "{{ test_ldap_use_tls | default(false) }}" + use_ldaps: false + state: present + register: ldap_setup_result + when: can_run_tests | bool + +- name: Verify LDAP was configured successfully + ansible.builtin.assert: + that: + - ldap_setup_result.ldaps is defined + - ldap_setup_result.ldaps.id is defined + fail_msg: "LDAP setup failed - cannot proceed with nonlocal group tests" + success_msg: "LDAP configured successfully" + when: ldap_setup_result is not skipped + +- name: Verify LDAP is connected (state from ldaps get, retry until CONNECTED) + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "{{ test_ldap_domain }}" + domain_name: "{{ test_ldap_domain }}" + state: present + register: ldap_status + when: ldap_setup_result is not skipped + retries: 6 + delay: 5 + until: ldap_status.ldaps.state == 'CONNECTED' + +- name: Assert LDAP reached CONNECTED state + ansible.builtin.assert: + that: + - ldap_status.ldaps.state == 'CONNECTED' + fail_msg: >- + LDAP is not connected. State: {{ ldap_status.ldaps.state | default('unknown') }}. + Expected CONNECTED. Check LDAP server reachability and credentials. + success_msg: "LDAP is connected and ready" + when: ldap_setup_result is not skipped + +# ==================== +# Connect default tenant to LDAP +# ==================== + +- name: Connect default tenant to LDAP + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "default" + ldap_provider_id: "{{ ldap_status.ldaps.id }}" + posix_primary_provider: "LDAP" + state: present + register: tenant_ldap_connect_result + when: ldap_setup_result is not skipped + +- name: Verify default tenant is connected to LDAP + ansible.builtin.assert: + that: + - tenant_ldap_connect_result.tenants is defined + - tenant_ldap_connect_result.tenants.ldap_provider_id == ldap_status.ldaps.id + fail_msg: "Failed to connect default tenant to LDAP" + success_msg: "Default tenant connected to LDAP" + when: ldap_setup_result is not skipped + +# ==================== +# Ensure test group exists in LDAP +# ==================== + +- name: Ensure test group exists in LDAP if not exists create it + ansible.builtin.command: + cmd: >- + ldapadd -x + -H {{ test_ldap_urls[0] }} + -D "{{ test_ldap_binddn }}" + -w "{{ test_ldap_bindpw }}" + stdin: | + dn: cn={{ test_nonlocal_groupname }},{{ test_ldap_searchbase }} + objectClass: posixGroup + objectClass: extensibleObject + objectClass: top + cn: {{ test_nonlocal_groupname }} + uid: {{ test_nonlocal_groupname }} + gidNumber: {{ test_nonlocal_gid }} + memberUid: {{ test_nonlocal_username }} + register: ldap_group_create_result + changed_when: ldap_group_create_result.rc == 0 + failed_when: ldap_group_create_result.rc not in [0, 68] + when: can_run_group_tests | bool + +- name: Ensure LDAP group has required attributes for VAST discovery (extensibleObject, uid, memberUid) + ansible.builtin.command: + cmd: >- + ldapmodify -x -c + -H {{ test_ldap_urls[0] }} + -D "{{ test_ldap_binddn }}" + -w "{{ test_ldap_bindpw }}" + stdin: | + dn: cn={{ test_nonlocal_groupname }},{{ test_ldap_searchbase }} + changetype: modify + add: objectClass + objectClass: extensibleObject + + dn: cn={{ test_nonlocal_groupname }},{{ test_ldap_searchbase }} + changetype: modify + add: uid + uid: {{ test_nonlocal_groupname }} + + dn: cn={{ test_nonlocal_groupname }},{{ test_ldap_searchbase }} + changetype: modify + add: memberUid + memberUid: {{ test_nonlocal_username }} + register: ldap_ensure_result + changed_when: ldap_ensure_result.rc == 0 + failed_when: ldap_ensure_result.rc not in [0, 20] + when: can_run_group_tests | bool + +# ==================== +# Query Tests - by groupname +# ==================== + +- name: Test querying a nonlocal group by groupname + vastdata.vms.nonlocal_group: + vms: "{{ vms_auth }}" + groupname: "{{ test_nonlocal_groupname }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result + until: result is not failed + when: can_run_group_tests | bool + +- name: Verify nonlocal group was returned + ansible.builtin.assert: + that: + - result.nonlocal_group is defined + - result.nonlocal_group != {} + - result.nonlocal_group.name is defined + fail_msg: "Nonlocal group query returned empty result - check that '{{ test_nonlocal_groupname }}' exists in LDAP" + success_msg: "Nonlocal group queried successfully" + when: result is not skipped + +# ==================== +# Consistency Tests - by groupname +# ==================== + +- name: Test consistency - query same group again by groupname + vastdata.vms.nonlocal_group: + vms: "{{ vms_auth }}" + groupname: "{{ test_nonlocal_groupname }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_idempotent + when: can_run_group_tests | bool + +- name: Verify consistency (data must be consistent across repeated queries) + ansible.builtin.assert: + that: + - result_idempotent.nonlocal_group is defined + - result_idempotent.nonlocal_group.name == result.nonlocal_group.name + - result_idempotent.nonlocal_group.gid == result.nonlocal_group.gid + fail_msg: "Consistency check failed - repeated query returned different data" + success_msg: "Consistency verified: repeated query returned consistent data" + when: result_idempotent is not skipped and result is not skipped + +# ==================== +# Query Tests - by GID +# ==================== + +- name: Test querying a nonlocal group by GID + vastdata.vms.nonlocal_group: + vms: "{{ vms_auth }}" + gid: "{{ test_nonlocal_gid }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_by_gid + when: can_run_group_tests | bool and test_nonlocal_gid is not none and test_nonlocal_gid != "" + +- name: Verify nonlocal group returned by GID matches groupname lookup + ansible.builtin.assert: + that: + - result_by_gid.nonlocal_group is defined + - result_by_gid.nonlocal_group != {} + - result_by_gid.nonlocal_group.gid == test_nonlocal_gid | int + fail_msg: "GID-based nonlocal group query returned unexpected result" + success_msg: "Nonlocal group queried by GID successfully" + when: result_by_gid is defined and result_by_gid is not skipped + +# ==================== +# Consistency Tests - by GID +# ==================== + +- name: Test consistency - query same group again by GID + vastdata.vms.nonlocal_group: + vms: "{{ vms_auth }}" + gid: "{{ test_nonlocal_gid }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_by_gid_idempotent + when: can_run_group_tests | bool and test_nonlocal_gid is not none and test_nonlocal_gid != "" + +- name: Verify consistency for GID-based query (data must be consistent across repeated queries) + ansible.builtin.assert: + that: + - result_by_gid_idempotent.nonlocal_group is defined + - result_by_gid_idempotent.nonlocal_group.gid == result_by_gid.nonlocal_group.gid + - result_by_gid_idempotent.nonlocal_group.name == result_by_gid.nonlocal_group.name + fail_msg: "Consistency check failed for GID-based query - repeated query returned different data" + success_msg: "Consistency verified for GID-based query" + when: result_by_gid_idempotent is defined and result_by_gid_idempotent is not skipped + +# ==================== +# Idempotency Tests +# ==================== + +- name: Test idempotency - first run with gid and tenant_id + vastdata.vms.nonlocal_group: + vms: "{{ vms_auth }}" + gid: "{{ test_nonlocal_gid }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_idempotent_first + when: can_run_group_tests | bool and test_nonlocal_gid is not none and test_nonlocal_gid != "" + +- name: Test idempotency - second run with same gid and tenant_id (should not report changed) + vastdata.vms.nonlocal_group: + vms: "{{ vms_auth }}" + gid: "{{ test_nonlocal_gid }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_idempotent_second + when: can_run_group_tests | bool and test_nonlocal_gid is not none and test_nonlocal_gid != "" + +- name: Verify idempotency - second run should NOT report changed + ansible.builtin.assert: + that: + - result_idempotent_second is not changed + - result_idempotent_second.nonlocal_group is defined + - result_idempotent_second.nonlocal_group.gid == test_nonlocal_gid | int + fail_msg: | + Second run with same gid and tenant_id reported changed=true. + This indicates tenant_id is being compared even though the API doesn't return it. + Expected: changed=false + Actual: changed={{ result_idempotent_second.changed }} + success_msg: "Idempotency verified: second run correctly reported changed=false" + when: result_idempotent_second is defined and result_idempotent_second is not skipped + +- name: Test idempotency with groupname - first run + vastdata.vms.nonlocal_group: + vms: "{{ vms_auth }}" + groupname: "{{ test_nonlocal_groupname }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_idempotent_groupname_first + when: can_run_group_tests | bool + +- name: Test idempotency with groupname - second run (should not report changed) + vastdata.vms.nonlocal_group: + vms: "{{ vms_auth }}" + groupname: "{{ test_nonlocal_groupname }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_idempotent_groupname_second + when: can_run_group_tests | bool + +- name: Verify idempotency with groupname - second run should NOT report changed + ansible.builtin.assert: + that: + - result_idempotent_groupname_second is not changed + - result_idempotent_groupname_second.nonlocal_group is defined + fail_msg: | + Second run with same groupname and tenant_id reported changed=true. + Expected: changed=false + Actual: changed={{ result_idempotent_groupname_second.changed }} + success_msg: "Idempotency with groupname verified: second run correctly reported changed=false" + when: result_idempotent_groupname_second is defined and result_idempotent_groupname_second is not skipped + +# ==================== +# Update Tests - s3_policies_ids +# ==================== + +- name: Test updating nonlocal group - set s3_policies_ids to empty + vastdata.vms.nonlocal_group: + vms: "{{ vms_auth }}" + groupname: "{{ test_nonlocal_groupname }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + s3_policies_ids: [] + state: present + register: result_patch + when: can_run_group_tests | bool + +- name: Re-query group to verify s3_policies_ids was actually updated + vastdata.vms.nonlocal_group: + vms: "{{ vms_auth }}" + groupname: "{{ test_nonlocal_groupname }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_patch_verify + when: result_patch is not skipped + +- name: Verify s3_policies_ids update + ansible.builtin.assert: + that: + - result_patch.nonlocal_group is defined + - result_patch_verify.nonlocal_group is defined + - result_patch_verify.nonlocal_group.name == test_nonlocal_groupname + - result_patch_verify.nonlocal_group.s3_policies_ids == [] + fail_msg: "PATCH s3_policies_ids failed - value not updated to empty" + success_msg: "s3_policies_ids update verified" + when: result_patch is not skipped + +# ==================== +# Consistency Tests - Update +# ==================== + +- name: Test update consistency - set s3_policies_ids to empty again + vastdata.vms.nonlocal_group: + vms: "{{ vms_auth }}" + groupname: "{{ test_nonlocal_groupname }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + s3_policies_ids: [] + state: present + register: result_patch_idempotent + when: can_run_group_tests | bool + +- name: Re-query group to verify s3_policies_ids is still empty after repeated PATCH + vastdata.vms.nonlocal_group: + vms: "{{ vms_auth }}" + groupname: "{{ test_nonlocal_groupname }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_patch_idempotent_verify + when: result_patch_idempotent is not skipped + +- name: Verify update consistency (repeated PATCH must return consistent data) + ansible.builtin.assert: + that: + - result_patch_idempotent_verify.nonlocal_group is defined + - result_patch_idempotent_verify.nonlocal_group.name == test_nonlocal_groupname + - result_patch_idempotent_verify.nonlocal_group.s3_policies_ids == [] + fail_msg: "Update consistency check failed - repeated PATCH returned unexpected data" + success_msg: "Update consistency verified" + when: result_patch_idempotent is not skipped + +# ==================== +# Cleanup +# ==================== + +- name: Final cleanup - remove test LDAP only if we created it this run + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "{{ test_ldap_domain }}" + state: absent + when: can_run_tests | bool and ldap_setup_result is defined and ldap_setup_result.changed | default(false) + failed_when: false + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping nonlocalgroups integration tests - credentials or LDAP configuration not set. + + To enable integration tests, set credentials and LDAP config. + For group-specific tests, also set: + TEST_NONLOCAL_GROUPNAME= # e.g. group from ldapsearch + TEST_NONLOCAL_GID= # optional, for GID-based tests + + Example: + VAST_VMS=10.95.87.91 TEST_NONLOCAL_GROUPNAME=mygroup ./test.sh integration nonlocalgroups + + Then run: ansible-test integration nonlocalgroups -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nonlocal_user/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nonlocal_user/defaults/main.yml new file mode 100644 index 000000000..eac668ed2 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nonlocal_user/defaults/main.yml @@ -0,0 +1,33 @@ +--- +# Default variables for nonlocalusers integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +# Leave empty to skip tests gracefully +vast_vms: "" +vast_username: "" +vast_password: "" + +# LDAP configuration these must point to a real LDAP directory accessible from the VAST system +test_ldap_domain: "" +test_ldap_searchbase: "" +test_ldap_binddn: "" +test_ldap_bindpw: "" +test_ldap_urls: [] +test_ldap_port: 389 +test_ldap_method: "simple" +test_ldap_use_tls: false + +# Test nonlocal user to query via LDAP this must be a valid user that exists in the configured LDAP directory +# Leave empty to skip nonlocal user lookup tests +test_nonlocal_username: "vast_ansible_user" +test_nonlocal_uid: 4288448012 +test_nonlocal_password: "Vastdata1!" + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nonlocal_user/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nonlocal_user/tasks/main.yml new file mode 100644 index 000000000..3f636b3c5 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/nonlocal_user/tasks/main.yml @@ -0,0 +1,521 @@ +--- +# Integration tests for nonlocal_user module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml +# Requires: LDAP server configured and accessible from VAST system + +# ==================== +# Setup +# ==================== + +- name: Check if VAST credentials are available + ansible.builtin.set_fact: + has_vast_credentials: >- + {{ + (vast_vms | default('') | length > 0) and + (vast_username | default('') | length > 0) and + (vast_password | default('') | length > 0) + }} + +- name: Check if LDAP configuration is available + ansible.builtin.set_fact: + has_ldap_config: >- + {{ + (test_ldap_domain | default('') | length > 0) and + (test_ldap_urls | default([]) | length > 0) + }} + +- name: Check if a test nonlocal username is configured + ansible.builtin.set_fact: + has_nonlocal_user: "{{ test_nonlocal_username | default('') | length > 0 }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_vast_credentials and has_ldap_config }}" + can_run_user_tests: "{{ has_vast_credentials and has_ldap_config and has_nonlocal_user }}" + +- name: Display test configuration + ansible.builtin.debug: + msg: | + ╔════════════════════════════════════════════════════════════════╗ + ║ Test Configuration ║ + ╚════════════════════════════════════════════════════════════════╝ + + VAST Connection: + VMS: {{ vast_vms | default('not set') }} + Username: {{ vast_username | default('not set') }} + Password: {{ '***set***' if vast_password else 'not set' }} + + LDAP Configuration: + Domain: {{ test_ldap_domain | default('not set') }} + URLs: {{ test_ldap_urls | default([]) | join(', ') }} + Port: {{ test_ldap_port | default(389) }} + Method: {{ test_ldap_method | default('simple') }} + Use TLS: {{ test_ldap_use_tls | default(false) }} + Search Base: {{ test_ldap_searchbase | default('not set') }} + Bind DN: {{ test_ldap_binddn | default('not set') }} + Bind Password: {{ '***set***' if test_ldap_bindpw else 'not set' }} + + Nonlocal User Test: + Username: {{ test_nonlocal_username | default('not set') }} + UID: {{ test_nonlocal_uid | default('not set') }} + when: can_run_tests + +# ==================== +# LDAP Setup +# ==================== + +- name: Ensure LDAP is configured (create if not exists, use if already configured) + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + domain_name: "{{ test_ldap_domain }}" + name: "{{ test_ldap_domain }}" + urls: "{{ test_ldap_urls }}" + searchbase: "{{ test_ldap_searchbase }}" + binddn: "{{ test_ldap_binddn }}" + bindpw: "{{ test_ldap_bindpw }}" + port: "{{ test_ldap_port }}" + method: "{{ test_ldap_method }}" + use_tls: "{{ test_ldap_use_tls | default(false) }}" + use_ldaps: false + state: present + register: ldap_setup_result + when: can_run_tests | bool + +- name: Verify LDAP was configured successfully + ansible.builtin.assert: + that: + - ldap_setup_result.ldaps is defined + - ldap_setup_result.ldaps.id is defined + fail_msg: "LDAP setup failed - cannot proceed with nonlocal user tests" + success_msg: "LDAP configured successfully" + when: ldap_setup_result is not skipped + +- name: Verify LDAP is connected (state from ldaps get, retry until CONNECTED) + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + name: "{{ test_ldap_domain }}" + domain_name: "{{ test_ldap_domain }}" + state: present + register: ldap_status + when: ldap_setup_result is not skipped + retries: 6 + delay: 5 + until: ldap_status.ldaps.state == 'CONNECTED' + +- name: Assert LDAP reached CONNECTED state + ansible.builtin.assert: + that: + - ldap_status.ldaps.state == 'CONNECTED' + fail_msg: >- + LDAP is not connected. State: {{ ldap_status.ldaps.state | default('unknown') }}. + Expected CONNECTED. Check LDAP server reachability and credentials. + success_msg: "LDAP is connected and ready" + when: ldap_setup_result is not skipped + +# ==================== +# Connect default tenant to LDAP +# ==================== + +- name: Connect default tenant to LDAP + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "default" + ldap_provider_id: "{{ ldap_status.ldaps.id }}" + posix_primary_provider: "LDAP" + state: present + register: tenant_ldap_connect_result + when: ldap_setup_result is not skipped + +- name: Verify default tenant is connected to LDAP + ansible.builtin.assert: + that: + - tenant_ldap_connect_result.tenants is defined + - tenant_ldap_connect_result.tenants.ldap_provider_id == ldap_status.ldaps.id + fail_msg: "Failed to connect default tenant to LDAP" + success_msg: "Default tenant connected to LDAP" + when: ldap_setup_result is not skipped + +# ==================== +# Ensure test user exists in LDAP +# ==================== + +- name: Ensure test user exists in LDAP directory (create if not present, rc=68 means already exists) + ansible.builtin.command: + cmd: >- + ldapadd -x + -H {{ test_ldap_urls[0] }} + -D "{{ test_ldap_binddn }}" + -w "{{ test_ldap_bindpw }}" + stdin: | + dn: uid={{ test_nonlocal_username }},{{ test_ldap_searchbase }} + objectClass: inetOrgPerson + objectClass: posixAccount + objectClass: shadowAccount + cn: {{ test_nonlocal_username }} + sn: ansible_user + givenName: vast + uid: {{ test_nonlocal_username }} + uidNumber: {{ test_nonlocal_uid }} + gidNumber: {{ test_nonlocal_uid }} + homeDirectory: /home/{{ test_nonlocal_username }} + loginShell: /bin/bash + mail: {{ test_nonlocal_username }}@{{ test_ldap_domain }} + userPassword: {{ test_nonlocal_password }} + register: ldap_user_create_result + changed_when: ldap_user_create_result.rc == 0 + failed_when: ldap_user_create_result.rc not in [0, 68] + when: can_run_user_tests | bool + +- name: Report whether LDAP user was created or already existed + ansible.builtin.debug: + msg: "{{ 'Created LDAP user: ' + test_nonlocal_username if ldap_user_create_result.changed else 'LDAP user already exists: ' + test_nonlocal_username }}" + when: ldap_user_create_result is not skipped + +# ==================== +# Query Tests - by username +# ==================== + +- name: Test querying a nonlocal user by username + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + username: "{{ test_nonlocal_username }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result + when: can_run_user_tests | bool + +- name: Verify nonlocal user was returned + ansible.builtin.assert: + that: + - result.nonlocal_user is defined + - result.nonlocal_user != {} + - result.nonlocal_user.name is defined + fail_msg: "Nonlocal user query returned empty result - check that '{{ test_nonlocal_username }}' exists in LDAP" + success_msg: "Nonlocal user queried successfully" + when: result is not skipped + +# ==================== +# Consistency Tests - by username +# ==================== + +- name: Query same user again by username (consistency check) + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + username: "{{ test_nonlocal_username }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_idempotent + when: can_run_user_tests | bool + +- name: Verify consistent results across queries + ansible.builtin.assert: + that: + - result_idempotent.nonlocal_user is defined + - result_idempotent.nonlocal_user != {} + - result_idempotent.nonlocal_user.name == result.nonlocal_user.name + - result_idempotent.nonlocal_user.uid == result.nonlocal_user.uid + fail_msg: "Consistency check failed - second query returned different user data" + success_msg: "Consistency verified: both queries returned the same user" + when: result_idempotent is not skipped and result is not skipped + +# ==================== +# Query Tests - by UID +# ==================== + +- name: Test querying a nonlocal user by UID + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + uid: "{{ test_nonlocal_uid }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_by_uid + when: can_run_user_tests | bool and test_nonlocal_uid is not none and test_nonlocal_uid != "" + +- name: Verify nonlocal user returned by UID matches username lookup + ansible.builtin.assert: + that: + - result_by_uid.nonlocal_user is defined + - result_by_uid.nonlocal_user != {} + - result_by_uid.nonlocal_user.uid == test_nonlocal_uid | int + fail_msg: "UID-based nonlocal user query returned unexpected result" + success_msg: "Nonlocal user queried by UID successfully" + when: result_by_uid is defined and result_by_uid is not skipped + +# ==================== +# Consistency Tests - by UID +# ==================== + +- name: Query same user again by UID (consistency check) + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + uid: "{{ test_nonlocal_uid }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_by_uid_idempotent + when: can_run_user_tests | bool and test_nonlocal_uid is not none and test_nonlocal_uid != "" + +- name: Verify consistent results across UID-based queries + ansible.builtin.assert: + that: + - result_by_uid_idempotent.nonlocal_user is defined + - result_by_uid_idempotent.nonlocal_user != {} + - result_by_uid_idempotent.nonlocal_user.uid == result_by_uid.nonlocal_user.uid + fail_msg: "Consistency check failed for UID-based query - second query returned different user data" + success_msg: "Consistency verified for UID-based query" + when: result_by_uid_idempotent is defined and result_by_uid_idempotent is not skipped + +# ==================== +# Idempotency Tests +# ==================== + +- name: Test idempotency - first run with uid and tenant_id + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + uid: "{{ test_nonlocal_uid }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_idempotent_first + when: can_run_user_tests | bool and test_nonlocal_uid is not none and test_nonlocal_uid != "" + +- name: Test idempotency - second run with same uid and tenant_id (should not report changed) + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + uid: "{{ test_nonlocal_uid }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_idempotent_second + when: can_run_user_tests | bool and test_nonlocal_uid is not none and test_nonlocal_uid != "" + +- name: Verify idempotency - second run should NOT report changed + ansible.builtin.assert: + that: + - result_idempotent_second is not changed + - result_idempotent_second.nonlocal_user is defined + - result_idempotent_second.nonlocal_user.uid == test_nonlocal_uid | int + fail_msg: | + Second run with same uid and tenant_id reported changed=true. + This indicates tenant_id is being compared even though the API doesn't return it. + Expected: changed=false + Actual: changed={{ result_idempotent_second.changed }} + success_msg: "Idempotency verified: second run correctly reported changed=false" + when: result_idempotent_second is defined and result_idempotent_second is not skipped + +- name: Test idempotency with username - first run + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + username: "{{ test_nonlocal_username }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_idempotent_username_first + when: can_run_user_tests | bool + +- name: Test idempotency with username - second run (should not report changed) + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + username: "{{ test_nonlocal_username }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_idempotent_username_second + when: can_run_user_tests | bool + +- name: Verify idempotency with username - second run should NOT report changed + ansible.builtin.assert: + that: + - result_idempotent_username_second is not changed + - result_idempotent_username_second.nonlocal_user is defined + fail_msg: | + Second run with same username and tenant_id reported changed=true. + Expected: changed=false + Actual: changed={{ result_idempotent_username_second.changed }} + success_msg: "Idempotency with username verified: second run correctly reported changed=false" + when: result_idempotent_username_second is defined and result_idempotent_username_second is not skipped + +# ==================== +# Update Tests - S3 permissions +# ==================== + +- name: Test updating nonlocal user - enable allow_create_bucket + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + username: "{{ test_nonlocal_username }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + allow_create_bucket: true + state: present + register: result_patch_create + when: can_run_user_tests | bool + +- name: Verify allow_create_bucket was updated + ansible.builtin.assert: + that: + - result_patch_create is changed + - result_patch_create.nonlocal_user is defined + fail_msg: "PATCH allow_create_bucket failed to execute" + success_msg: "PATCH allow_create_bucket executed" + when: result_patch_create is not skipped + +- name: Re-query user to confirm allow_create_bucket is now true + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + username: "{{ test_nonlocal_username }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_verify_create + when: can_run_user_tests | bool + +- name: Assert allow_create_bucket is true after update + ansible.builtin.assert: + that: + - result_verify_create.nonlocal_user.allow_create_bucket == true + fail_msg: "allow_create_bucket is not true after PATCH" + success_msg: "allow_create_bucket confirmed true" + when: result_verify_create is not skipped + +# ==================== +# Update Tests - allow_delete_bucket +# ==================== + +- name: Test updating nonlocal user - enable allow_delete_bucket + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + username: "{{ test_nonlocal_username }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + allow_delete_bucket: true + state: present + register: result_patch_delete + when: can_run_user_tests | bool + +- name: Verify allow_delete_bucket was updated + ansible.builtin.assert: + that: + - result_patch_delete is changed + - result_patch_delete.nonlocal_user is defined + fail_msg: "PATCH allow_delete_bucket failed to execute" + success_msg: "PATCH allow_delete_bucket executed" + when: result_patch_delete is not skipped + +- name: Re-query user to confirm allow_delete_bucket is now true + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + username: "{{ test_nonlocal_username }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_verify_delete + when: can_run_user_tests | bool + +- name: Assert allow_delete_bucket is true after update + ansible.builtin.assert: + that: + - result_verify_delete.nonlocal_user.allow_delete_bucket == true + fail_msg: "allow_delete_bucket is not true after PATCH" + success_msg: "allow_delete_bucket confirmed true" + when: result_verify_delete is not skipped + +# ==================== +# Update Tests - s3_superuser +# ==================== + +- name: Test updating nonlocal user - enable s3_superuser + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + username: "{{ test_nonlocal_username }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + s3_superuser: true + state: present + register: result_patch_superuser + when: can_run_user_tests | bool + +- name: Verify s3_superuser was updated + ansible.builtin.assert: + that: + - result_patch_superuser is changed + - result_patch_superuser.nonlocal_user is defined + fail_msg: "PATCH s3_superuser failed to execute" + success_msg: "PATCH s3_superuser executed" + when: result_patch_superuser is not skipped + +- name: Re-query user to confirm s3_superuser is now true + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + username: "{{ test_nonlocal_username }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_verify_superuser + when: can_run_user_tests | bool + +- name: Assert s3_superuser is true after update + ansible.builtin.assert: + that: + - result_verify_superuser.nonlocal_user.s3_superuser == true + fail_msg: "s3_superuser is not true after PATCH" + success_msg: "s3_superuser confirmed true" + when: result_verify_superuser is not skipped + +# ==================== +# Restore Tests - reset S3 permissions +# ==================== + +- name: Restore S3 permissions to false + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + username: "{{ test_nonlocal_username }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + allow_create_bucket: false + allow_delete_bucket: false + s3_superuser: false + state: present + register: result_patch_restore + when: can_run_user_tests | bool + +- name: Verify S3 permissions restore PATCH executed + ansible.builtin.assert: + that: + - result_patch_restore is changed + - result_patch_restore.nonlocal_user is defined + fail_msg: "Restoring S3 permissions PATCH failed to execute" + success_msg: "Restore PATCH executed" + when: result_patch_restore is not skipped + +- name: Re-query user to confirm S3 permissions were restored + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + username: "{{ test_nonlocal_username }}" + tenant_id: "{{ tenant_ldap_connect_result.tenants.id }}" + state: present + register: result_verify_restore + when: can_run_user_tests | bool + +- name: Assert S3 permissions are restored to false + ansible.builtin.assert: + that: + - result_verify_restore.nonlocal_user.allow_create_bucket == false + - result_verify_restore.nonlocal_user.allow_delete_bucket == false + - result_verify_restore.nonlocal_user.s3_superuser == false + fail_msg: "S3 permissions were not restored correctly" + success_msg: "S3 permissions restored successfully" + when: result_verify_restore is not skipped + +# ==================== +# Cleanup +# ==================== + +- name: Final cleanup - remove test LDAP only if we created it this run + vastdata.vms.ldaps: + vms: "{{ vms_auth }}" + domain_name: "{{ test_ldap_domain }}" + state: absent + when: can_run_tests | bool and ldap_setup_result is defined and ldap_setup_result.changed | default(false) + failed_when: false + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping nonlocalusers integration tests - credentials or LDAP configuration not set. + + To enable integration tests, set credentials and LDAP config in: + tests/integration/integration_config.yml + + Then run: ansible-test integration nonlocalusers -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/protectedpaths/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/protectedpaths/defaults/main.yml new file mode 100644 index 000000000..0701d6423 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/protectedpaths/defaults/main.yml @@ -0,0 +1,32 @@ +--- +# Default variables for protectedpaths integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +# Leave empty to skip tests gracefully +vast_vms: "" +vast_username: "" +vast_password: "" + +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true + +# Test protectedpath configuration +test_protectedpath_name: "ansible-test-protectedpath" +test_protectedpath_name_updated: "ansible-test-protectedpath-updated" + +# Source directory configuration +test_source_dir: "/ansible-test-pp-source" +test_target_exported_dir: "/ansible-test-pp-target" + +# View configuration for source directory +test_view_path: "/ansible-test-pp-view" +test_view_policy_id: 1 + +# Protected path parameters +test_capabilities: "ASYNC_REPLICATION" +test_lease_expiry_time: 3600 diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/protectedpaths/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/protectedpaths/tasks/main.yml new file mode 100644 index 000000000..e4937d709 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/protectedpaths/tasks/main.yml @@ -0,0 +1,628 @@ +--- +# Integration tests for protectedpaths module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials }}" + +- name: Generate unique protectedpath names for this test run + ansible.builtin.set_fact: + test_protectedpath_name_unique: "{{ test_protectedpath_name }}-{{ ansible_date_time.epoch }}" + test_protectedpath_name_updated_unique: "{{ test_protectedpath_name_updated }}-{{ ansible_date_time.epoch }}" + when: can_run_tests | bool + +# ==================== +# Setup - Create Prerequisites +# ==================== + +- name: Get default tenant for testing + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "default" + state: present + register: tenant_result + when: can_run_tests | bool + failed_when: false + +- name: Set default tenant ID from result + ansible.builtin.set_fact: + test_tenant_id: "{{ tenant_result.tenants.id }}" + when: can_run_tests | bool and tenant_result.tenants is defined + +- name: Create test protection policy for protectedpath testing + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "ansible-test-protection-policy-{{ ansible_date_time.epoch }}" + prefix: "snap-{{ ansible_date_time.epoch }}" + clone_type: "LOCAL" + frames: + - every: "1h" + keep-local: "1h" + indestructible: false + state: present + register: test_policy_result + when: can_run_tests | bool and (test_protection_policy_id is not defined or test_protection_policy_id == '') + failed_when: false + +- name: Set test protection policy ID from created policy + ansible.builtin.set_fact: + test_protection_policy_id: "{{ test_policy_result.protectionpolicies.id }}" + when: test_policy_result is not skipped and test_policy_result.protectionpolicies is defined + +- name: Skip tests if no protection policy could be created or found + ansible.builtin.set_fact: + can_run_tests: false + when: can_run_tests | bool and (test_protection_policy_id is not defined or test_protection_policy_id == '') + +- name: Debug protection policy status + ansible.builtin.debug: + msg: "Using protection policy ID: {{ test_protection_policy_id }}" + when: can_run_tests | bool and test_protection_policy_id is defined + +- name: Create test view for source directory + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_source_dir }}" + policy_id: "{{ test_view_policy_id }}" + protocols: ["NFS"] + create_dir: true + state: present + register: test_view + when: can_run_tests | bool + failed_when: false + +- name: Get view ID for protectedpath creation + ansible.builtin.set_fact: + test_view_id: "{{ test_view.views.id }}" + when: test_view is not skipped and test_view.views is defined + +# ==================== +# Cleanup - Remove existing test protectedpaths if present +# ==================== + +- name: Clean up - ensure test protectedpath doesn't exist before starting + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +- name: Clean up - ensure updated test protectedpath doesn't exist + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_updated_unique }}" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +- name: Clean up - ensure delcheck test protectedpath doesn't exist + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}-delcheck" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +- name: Wait for cleanup to complete (async delete) + ansible.builtin.pause: + seconds: 5 + when: can_run_tests | bool and cleanup_result is changed + +# ==================== +# Create Tests +# ==================== + +- name: Test creating a protectedpath + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + source_dir: "{{ test_source_dir }}" + target_exported_dir: "{{ test_target_exported_dir }}" + tenant_id: "{{ test_tenant_id }}" + capabilities: "{{ test_capabilities }}" + protection_policy_id: "{{ test_protection_policy_id }}" # Add this line + enabled: true + state: present + register: result + when: can_run_tests | bool and test_tenant_id is defined + +- name: Verify protectedpath was created + ansible.builtin.assert: + that: + - result is changed + - result.protectedpaths is defined + - result.protectedpaths.name == test_protectedpath_name_unique + - result.protectedpaths.id is defined + - result.diff is defined + - result.diff.before == {} + - result.diff.after is defined + when: result is not skipped + +- name: Save protectedpath ID for later tests + ansible.builtin.set_fact: + test_protectedpath_id: "{{ result.protectedpaths.id }}" + when: result is not skipped + +# ==================== +# Idempotency Tests - Create +# ==================== + +- name: Test idempotency - create same protectedpath again + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + source_dir: "{{ test_source_dir }}" + target_exported_dir: "{{ test_target_exported_dir }}" + tenant_id: "{{ test_tenant_id }}" + capabilities: "{{ test_capabilities }}" + enabled: true + state: present + register: result + when: can_run_tests | bool and test_protectedpath_id is defined + +- name: Verify idempotency (should not change) + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +- name: Test idempotency - rerun exact same playbook with immutable fields + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + source_dir: "{{ test_source_dir }}" + target_exported_dir: "{{ test_target_exported_dir }}" + tenant_id: "{{ test_tenant_id }}" + capabilities: "{{ test_capabilities }}" + protection_policy_id: "{{ test_protection_policy_id }}" + enabled: true + state: present + register: result + when: can_run_tests | bool and test_protectedpath_id is defined + +- name: Verify idempotency with immutable fields (should not change) + ansible.builtin.assert: + that: + - result is not changed + fail_msg: "Idempotency failed when re-running with immutable fields (protection_policy_id, target_exported_dir)" + when: result is not skipped + +# ==================== +# Update Tests - Enable/Disable +# ==================== + +- name: Test updating protectedpath - disable + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + enabled: false + state: present + register: result + when: can_run_tests | bool and test_protectedpath_id is defined + +- name: Verify protectedpath was disabled + ansible.builtin.assert: + that: + - result is changed + - result.protectedpaths is defined + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: result is not skipped + +- name: Test update idempotency - disable + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + enabled: false + state: present + register: result + when: can_run_tests | bool and test_protectedpath_id is defined + +- name: Verify update idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +- name: Test updating protectedpath - re-enable + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + enabled: true + state: present + register: result + when: can_run_tests | bool and test_protectedpath_id is defined + +- name: Verify protectedpath was re-enabled + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +# ==================== +# Update Tests - Lease Expiry Time +# ==================== + +- name: Test updating protectedpath - set lease expiry time + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + lease_expiry_time: "{{ test_lease_expiry_time }}" + state: present + register: result + when: can_run_tests | bool and test_protectedpath_id is defined + +- name: Verify lease expiry time was updated + ansible.builtin.assert: + that: + - result is changed + - result.protectedpaths is defined + - result.diff is defined + when: result is not skipped + +- name: Test update idempotency - lease expiry time + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + lease_expiry_time: "{{ test_lease_expiry_time }}" + state: present + register: result + when: can_run_tests | bool and test_protectedpath_id is defined + +- name: Verify update idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Lookup by ID Tests +# ==================== + +- name: Test lookup by ID + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + id: "{{ test_protectedpath_id }}" + state: present + register: result + when: can_run_tests | bool and test_protectedpath_id is defined + +- name: Verify lookup by ID works + ansible.builtin.assert: + that: + - result is not changed + - result.protectedpaths is defined + - result.protectedpaths.id == test_protectedpath_id + - result.protectedpaths.name == test_protectedpath_name_unique + when: result is not skipped + +# ==================== +# Check Mode Tests - Create +# ==================== + +- name: Test check mode - create new protectedpath + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}-checkmode" + source_dir: "{{ test_source_dir }}" + target_exported_dir: "{{ test_target_exported_dir }}" + tenant_id: "{{ test_tenant_id }}" + capabilities: "{{ test_capabilities }}" + state: present + check_mode: true + register: result + when: can_run_tests | bool and test_tenant_id is defined + +- name: Verify check mode create reported changes + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually create the protectedpath + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}-checkmode" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify protectedpath was never created + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Check Mode Tests - Update +# ==================== + +- name: Test check mode - update existing protectedpath + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + enabled: false + state: present + check_mode: true + register: result + when: can_run_tests | bool and test_protectedpath_id is defined + +- name: Verify check mode update reported changes + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually update + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + state: present + register: result + when: can_run_tests | bool and test_protectedpath_id is defined + +- name: Verify protectedpath was not updated by check mode + ansible.builtin.assert: + that: + - result.protectedpaths.enabled == true + when: result is not skipped + +# ==================== +# Check Mode Tests - Lookup +# ==================== + +- name: Test check mode - lookup existing resource + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + state: present + check_mode: true + register: result + when: can_run_tests | bool and test_protectedpath_id is defined + +- name: Verify check mode lookup works + ansible.builtin.assert: + that: + - result is not changed + - result.protectedpaths is defined + when: result is not skipped + +# ==================== +# Delete Tests +# ==================== + +- name: Test deleting protectedpath + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + state: absent + register: result + when: can_run_tests | bool and test_protectedpath_id is defined + +- name: Verify protectedpath was deleted + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after == {} + when: result is not skipped + +# ==================== +# Idempotency Tests - Delete +# ==================== + +- name: Test delete idempotency - delete again + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Check Mode Delete Test +# ==================== + +- name: Create protectedpath for check mode delete test + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}-delcheck" + source_dir: "{{ test_source_dir }}" + target_exported_dir: "{{ test_target_exported_dir }}" + tenant_id: "{{ test_tenant_id }}" + capabilities: "{{ test_capabilities }}" + protection_policy_id: "{{ test_protection_policy_id }}" + state: present + register: result + when: can_run_tests | bool and test_tenant_id is defined + +- name: Test check mode - delete + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}-delcheck" + state: absent + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported deletion + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually delete + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}-delcheck" + state: present + register: result + when: can_run_tests | bool + +- name: Check that protectedpath still exists + ansible.builtin.assert: + that: + - result is not changed + - result.protectedpaths is defined + when: result is not skipped + +- name: Clean up check mode delete test protectedpath + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}-delcheck" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Error Handling Tests +# ==================== + +- name: Test error handling - missing name and ID + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + source_dir: "{{ test_source_dir }}" + state: present + register: result + when: can_run_tests | bool + ignore_errors: true + +- name: Verify error was caught + ansible.builtin.assert: + that: + - result is failed + when: result is not skipped + +- name: Test error handling - invalid state + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + state: invalid + register: result + when: can_run_tests | bool + ignore_errors: true + +- name: Verify error was caught + ansible.builtin.assert: + that: + - result is failed + when: result is not skipped + +- name: Test error handling - missing required fields on create + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}-nofields" + state: present + register: result + when: can_run_tests | bool + ignore_errors: true + +- name: Verify error was caught for missing fields + ansible.builtin.assert: + that: + - result is failed + when: result is not skipped + +# ==================== +# Cleanup Prerequisites +# ==================== + +- name: Clean up test view + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_source_dir }}" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Final Cleanup +# ==================== + +- name: Final cleanup - remove test protectedpath if it exists + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove updated test protectedpath if it exists + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_updated_unique }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove delcheck test protectedpath if it exists + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}-delcheck" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove checkmode test protectedpath if it exists + vastdata.vms.protectedpaths: + vms: "{{ vms_auth }}" + name: "{{ test_protectedpath_name_unique }}-checkmode" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove test view if it exists + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_source_dir }}" + state: absent + when: test_view is defined and test_view is changed + failed_when: false + +- name: Final cleanup - remove test protection policy if it was created + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "ansible-test-protection-policy-{{ ansible_date_time.epoch }}" + state: absent + when: test_policy_result is defined and test_policy_result is changed + failed_when: false + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping protectedpaths integration tests - credentials not configured. + + To enable integration tests, set credentials in: + tests/integration/integration_config.yml + + Example: + vast_vms: "vast-vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration protectedpaths -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/protectionpolicies/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/protectionpolicies/defaults/main.yml new file mode 100644 index 000000000..0414b1737 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/protectionpolicies/defaults/main.yml @@ -0,0 +1,42 @@ +--- +# Default variables for protectionpolicies integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +vast_vms: "" +vast_username: "" +vast_password: "" + +# Protection policy test configuration +test_protectionpolicy_name: "ansible-test-protection-policy" +test_protectionpolicy_prefix: "ansible-test-snapshot" +test_protectionpolicy_clone_type: "LOCAL" +test_protectionpolicy_frames: + - every: "1d" + keep-local: "1d" + start-at: "2026-04-14 00:00:00" +test_protectionpolicy_updated_prefix: "ansible-test-snapshot-updated" + + +test_protectionpolicy_unit_norm_frames: + - every: "7D" + keep-local: "14D" + keep-remote: "0s" + start-at: "2026-04-14 09:00:00" + - every: "7D" + keep-local: "8D" + keep-remote: "0s" + start-at: "2026-04-14 09:00:00" + +test_protectionpolicy_default_keepremote_frames: + - every: "7D" + keep-local: "14D" + start-at: "2026-04-14 09:00:00" + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/protectionpolicies/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/protectionpolicies/tasks/main.yml new file mode 100644 index 000000000..2773335d4 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/protectionpolicies/tasks/main.yml @@ -0,0 +1,584 @@ +--- +# Integration tests for protectionpolicies module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials }}" + +# ==================== +# Cleanup - Remove existing test protection policy if present +# ==================== + +- name: Clean up - ensure test protection policy doesn't exist before starting + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +# ==================== +# Create Tests +# ==================== + +- name: Test creating a protection policy + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}" + prefix: "{{ test_protectionpolicy_prefix }}" + clone_type: "{{ test_protectionpolicy_clone_type }}" + frames: "{{ test_protectionpolicy_frames }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify protection policy was created + ansible.builtin.assert: + that: + - result is changed + - result.protectionpolicies is defined + - result.protectionpolicies.name == test_protectionpolicy_name + - result.protectionpolicies.prefix == test_protectionpolicy_prefix + - result.protectionpolicies.clone_type == test_protectionpolicy_clone_type + - result.diff is defined + - result.diff.before == {} + - result.diff.after is defined + when: result is not skipped + +# ==================== +# Idempotency Tests - Create +# ==================== + +- name: Test idempotency - create same protection policy again + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify idempotency (should not change) + ansible.builtin.assert: + that: + - result is not changed + - result.protectionpolicies is defined + when: result is not skipped + +# ==================== +# Update Tests +# ==================== + +- name: Test updating protection policy - change prefix + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}" + prefix: "{{ test_protectionpolicy_updated_prefix }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify protection policy was updated + ansible.builtin.assert: + that: + - result is changed + - result.protectionpolicies is defined + - result.protectionpolicies.prefix == test_protectionpolicy_updated_prefix + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: result is not skipped + +- name: Test update idempotency + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}" + prefix: "{{ test_protectionpolicy_updated_prefix }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify update idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Check Mode Tests - Update +# ==================== + +- name: Test check mode - update prefix + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}" + prefix: "ansible-check-prefix" + state: present + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported changes + ansible.builtin.assert: + that: + - result is changed + - result.protectionpolicies is defined + when: result is not skipped + +- name: Verify check mode didn't actually make changes + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}" + prefix: "{{ test_protectionpolicy_updated_prefix }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify prefix unchanged by check mode + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Check Mode Tests - Create +# ==================== + +- name: Test check mode - create new protection policy + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-checkmode" + prefix: "checkmode-prefix" + clone_type: "LOCAL" + frames: "{{ test_protectionpolicy_frames }}" + state: present + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode create reported changes + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually create the protection policy + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-checkmode" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify protection policy was never created + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Diff Mode Tests +# ==================== + +- name: Update protection policy with diff mode + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}" + prefix: "ansible-diff-prefix" + state: present + diff: true + register: result + when: can_run_tests | bool + +- name: Verify update diff output + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: result is not skipped + +# ==================== +# Indestructible Flag Tests +# ==================== + +- name: Test updating indestructible flag + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}" + indestructible: false + state: present + register: result + when: can_run_tests | bool + +- name: Verify indestructible flag was set + ansible.builtin.assert: + that: + - result.protectionpolicies is defined + - result.protectionpolicies.indestructible == false + when: result is not skipped + +# ==================== +# Delete Tests +# ==================== + +- name: Test deleting protection policy + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify protection policy was deleted + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after == {} + when: result is not skipped + +# ==================== +# Idempotency Tests - Delete +# ==================== + +- name: Test delete idempotency - delete again + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Check Mode Delete Test +# ==================== + +- name: Create protection policy for check mode delete test + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-delcheck" + prefix: "delcheck-prefix" + clone_type: "LOCAL" + frames: "{{ test_protectionpolicy_frames }}" + state: present + register: result + when: can_run_tests | bool + +- name: Test check mode - delete + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-delcheck" + state: absent + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported deletion + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually delete + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-delcheck" + state: present + register: result + when: can_run_tests | bool + +- name: Check that protection policy still exists + ansible.builtin.assert: + that: + - result is not changed + - result.protectionpolicies is defined + when: result is not skipped + +- name: Clean up check mode delete test protection policy + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-delcheck" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Error Handling Tests +# ==================== + +- name: Test error handling - invalid state + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}" + state: invalid + register: result + when: can_run_tests | bool + ignore_errors: true + +- name: Verify error was caught + ansible.builtin.assert: + that: + - result is failed + when: result is not skipped + +# ==================== +# ID-based Lookup Tests +# ==================== + +- name: Create protection policy for ID-based lookup test + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-idtest" + prefix: "idtest-prefix" + clone_type: "LOCAL" + frames: "{{ test_protectionpolicy_frames }}" + state: present + register: create_result + when: can_run_tests | bool + +- name: Test updating protection policy by ID + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + id: "{{ create_result.protectionpolicies.id }}" + prefix: "idtest-prefix-updated" + state: present + register: result + when: can_run_tests | bool and create_result is not skipped + +- name: Verify protection policy was updated by ID + ansible.builtin.assert: + that: + - result is changed + - result.protectionpolicies is defined + - result.protectionpolicies.prefix == "idtest-prefix-updated" + when: result is not skipped + +- name: Clean up ID-based lookup test protection policy + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-idtest" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================================================================== +# Frames Idempotency Tests +# ==================================================================== + +- name: Frames idempotency / cleanup - ensure unit-norm policy doesn't exist before starting + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-unit-norm" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Frames idempotency / create policy with normalize-prone frames (7D, 14D) + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-unit-norm" + prefix: "unit-norm-prefix" + clone_type: "LOCAL" + indestructible: false + frames: "{{ test_protectionpolicy_unit_norm_frames }}" + state: present + register: result + when: can_run_tests | bool + +- name: Frames idempotency / verify create reported changed + ansible.builtin.assert: + that: + - result is changed + - result.protectionpolicies is defined + when: result is not skipped + +- name: Frames idempotency / poll until server normalizes durations (7D -> 1W, 14D -> 2W) + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-unit-norm" + state: present + register: norm_check + until: + - norm_check.protectionpolicies is defined + - (norm_check.protectionpolicies.frames | default([]) | length) > 0 + - norm_check.protectionpolicies.frames[0].every | default('') == '1W' + - norm_check.protectionpolicies.frames[0]['keep-local'] | default('') == '2W' + retries: 15 + delay: 1 + when: can_run_tests | bool + +- name: Frames idempotency / re-run identical playbook — must report not changed + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-unit-norm" + prefix: "unit-norm-prefix" + clone_type: "LOCAL" + indestructible: false + frames: "{{ test_protectionpolicy_unit_norm_frames }}" + state: present + register: result + when: can_run_tests | bool + +- name: Frames idempotency / verify no PATCH after server normalization + ansible.builtin.assert: + that: + - result is not changed + fail_msg: >- + Frames idempotency regression: re-running an unchanged protection-policy + playbook issued a redundant PATCH after the server normalized duration + units (7D -> 1W, 14D -> 2W). The frames normalizer is not handling unit + equivalence correctly. + when: result is not skipped + +- name: Frames idempotency / cleanup - ensure default-keepremote policy doesn't exist + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-default-keepremote" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Frames idempotency / create policy with keep-remote omitted + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-default-keepremote" + prefix: "default-keepremote-prefix" + clone_type: "LOCAL" + frames: "{{ test_protectionpolicy_default_keepremote_frames }}" + state: present + register: result + when: can_run_tests | bool + +- name: Frames idempotency / verify create reported changed (defaults case) + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Frames idempotency / poll until server injects default keep-remote (defaults case) + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-default-keepremote" + state: present + register: norm_check + until: + - norm_check.protectionpolicies is defined + - (norm_check.protectionpolicies.frames | default([]) | length) > 0 + - "'keep-remote' in (norm_check.protectionpolicies.frames[0] | default({}))" + retries: 15 + delay: 1 + when: can_run_tests | bool + +- name: Frames idempotency / re-run with keep-remote still omitted — must be not changed + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-default-keepremote" + prefix: "default-keepremote-prefix" + clone_type: "LOCAL" + frames: "{{ test_protectionpolicy_default_keepremote_frames }}" + state: present + register: result + when: can_run_tests | bool + +- name: Frames idempotency / verify idempotency when keep-remote is server-defaulted + ansible.builtin.assert: + that: + - result is not changed + fail_msg: >- + Frames idempotency regression: re-running with `keep-remote` omitted + issued a redundant PATCH because the server-added "keep-remote: 0s" + was treated as a real diff. + when: result is not skipped + +# ==================== +# Final Cleanup +# ==================== + +- name: Final cleanup - remove test protection policy if it exists + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove delcheck test protection policy if it exists + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-delcheck" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove idtest protection policy if it exists + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-idtest" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove bigcatalog test protection policy if it exists + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-bigcatalog" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove unit-norm idempotency policy if it exists + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-unit-norm" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove default-keepremote idempotency policy if it exists + vastdata.vms.protectionpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_protectionpolicy_name }}-default-keepremote" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Skip Message +# ==================== + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping protectionpolicies integration tests - credentials not configured. + + To enable integration tests, set credentials in: + tests/integration/integration_config.yml + + Example: + vast_vms: "vast-vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration protectionpolicies -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/quotas/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/quotas/defaults/main.yml new file mode 100644 index 000000000..8356085a6 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/quotas/defaults/main.yml @@ -0,0 +1,25 @@ +--- +# Default variables for quotas integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +vast_vms: "" +vast_username: "" +vast_password: "" + +# Quota test configuration +test_quota_name: "ansible-test-quota" +test_quota_path: "/ansible-test-quota" +test_quota_hard_limit: 10737418240 +test_quota_soft_limit: 8589934592 +test_quota_grace_period: "7 00:00:00" +test_quota_hard_limit_updated: 21474836480 +test_quota_soft_limit_updated: 17179869184 + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/quotas/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/quotas/tasks/main.yml new file mode 100644 index 000000000..ecb7bd917 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/quotas/tasks/main.yml @@ -0,0 +1,513 @@ +--- +# Integration tests for quotas module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials | bool }}" + +# ==================== +# Cleanup - Ensure clean state +# ==================== + +- name: Clean up - ensure test quota doesn't exist before starting + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +# ==================== +# Create Tests +# ==================== + +- name: Test creating a quota + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + name: "{{ test_quota_name }}" + path: "{{ test_quota_path }}" + hard_limit: "{{ test_quota_hard_limit }}" + soft_limit: "{{ test_quota_soft_limit }}" + grace_period: "{{ test_quota_grace_period }}" + create_dir: true + state: present + register: result + when: can_run_tests | bool + +- name: Verify quota was created + ansible.builtin.assert: + that: + - result is changed + - result.quotas is defined + - result.quotas.name == test_quota_name + - result.quotas.path == test_quota_path + when: can_run_tests | bool + +# ==================== +# Idempotency Tests +# ==================== + +- name: Test idempotency - create same quota again + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + name: "{{ test_quota_name }}" + path: "{{ test_quota_path }}" + hard_limit: "{{ test_quota_hard_limit }}" + soft_limit: "{{ test_quota_soft_limit }}" + grace_period: "{{ test_quota_grace_period }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify idempotency (should not change) + ansible.builtin.assert: + that: + - result is not changed + when: can_run_tests | bool + +# ==================== +# Update Tests +# ==================== + +- name: Test updating quota - change hard_limit + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}" + hard_limit: "{{ test_quota_hard_limit_updated }}" + soft_limit: "{{ test_quota_soft_limit_updated }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify quota was updated + ansible.builtin.assert: + that: + - result is changed + when: can_run_tests | bool + +- name: Test update idempotency + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}" + hard_limit: "{{ test_quota_hard_limit_updated }}" + soft_limit: "{{ test_quota_soft_limit_updated }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify update idempotency + ansible.builtin.assert: + that: + - result is not changed + when: can_run_tests | bool + +# ==================== +# Update Enable Alarms +# ==================== + +- name: Test updating enable_alarms + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}" + enable_alarms: false + state: present + register: result + when: can_run_tests | bool + +- name: Verify enable_alarms was updated + ansible.builtin.assert: + that: + - result is changed + - result.quotas.enable_alarms == false + when: can_run_tests | bool + +# ==================== +# Check Mode Tests +# ==================== + +- name: Set baseline for check mode test + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}" + hard_limit: "{{ test_quota_hard_limit_updated }}" + soft_limit: "{{ test_quota_soft_limit_updated }}" + enable_alarms: false + state: present + register: baseline_result + when: can_run_tests | bool + +- name: Test check mode - update hard_limit + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}" + hard_limit: 53687091200 + state: present + register: result + check_mode: true + when: can_run_tests | bool + +- name: Verify check mode reported change + ansible.builtin.assert: + that: + - result is changed + when: can_run_tests | bool + +- name: Verify check mode didn't actually change + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}" + hard_limit: "{{ test_quota_hard_limit_updated }}" + state: present + register: result + when: can_run_tests | bool + +- name: Check that hard_limit was not changed + ansible.builtin.assert: + that: + - result is not changed + when: can_run_tests | bool + +# ==================== +# Diff Mode Tests +# ==================== + +- name: Test diff mode - update soft_limit + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}" + soft_limit: 12884901888 + state: present + register: result + diff: true + when: can_run_tests | bool + +- name: Verify diff mode returned diff data + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: can_run_tests | bool + +# ==================== +# Delete Tests +# ==================== + +- name: Test deleting quota + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify quota was deleted + ansible.builtin.assert: + that: + - result is changed + when: can_run_tests | bool + +# ==================== +# Delete Idempotency Test +# ==================== + +- name: Test delete idempotency - delete again + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed + when: can_run_tests | bool + +# ==================== +# Check Mode Delete Tests +# ==================== + +- name: Create quota for check mode delete test + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + name: "{{ test_quota_name }}-delcheck" + path: "{{ test_quota_path }}-delcheck" + hard_limit: "{{ test_quota_hard_limit }}" + create_dir: true + state: present + register: result + when: can_run_tests | bool + +- name: Test check mode - delete + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}-delcheck" + state: absent + register: result + check_mode: true + when: can_run_tests | bool + +- name: Verify check mode reported deletion + ansible.builtin.assert: + that: + - result is changed + when: can_run_tests | bool + +- name: Verify check mode didn't actually delete + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}-delcheck" + hard_limit: "{{ test_quota_hard_limit }}" + state: present + register: result + when: can_run_tests | bool + +- name: Check that quota still exists + ansible.builtin.assert: + that: + - result is not changed + when: can_run_tests | bool + +- name: Clean up check mode delete test quota + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}-delcheck" + state: absent + register: result + when: can_run_tests | bool + failed_when: false + +# ==================== +# Error Handling Tests +# ==================== + +- name: Test error handling - invalid state + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}" + state: invalid + register: result + ignore_errors: true + when: can_run_tests | bool + +- name: Verify error was caught + ansible.builtin.assert: + that: + - result is failed + - "'invalid' in result.msg" + when: can_run_tests | bool + +# ==================== +# Idempotency Tests - user_quotas and group_quotas +# ==================== + +- name: Clean up - ensure user_quotas/group_quotas test quota doesn't exist + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "/ansible-test-quota-nested" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up - remove previous test user if exists + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "test-user" + local_provider_id: 1 + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up - remove previous test group if exists + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "test-group" + local_provider_id: 1 + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Create test user for user_quotas test + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "test-user" + uid: 77777 + local_provider_id: 1 + state: present + register: test_user + when: can_run_tests | bool + +- name: Create test group for group_quotas test + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "test-group" + gid: 77778 + local_provider_id: 1 + state: present + register: test_group + when: can_run_tests | bool + +- name: Create quota with user_quotas and group_quotas + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + name: "test-quota-nested" + path: "/ansible-test-quota-nested" + tenant_id: 1 + soft_limit: 100000 + hard_limit: 100000 + is_user_quota: true + enable_alarms: true + default_email: "user@example.com" + user_quotas: + - identifier: "test-user" + identifier_type: "username" + hard_limit: 15000 + soft_limit: 15000 + grace_period: "02:00:00" + email: "user1@example.com" + group_quotas: + - identifier: "test-group" + identifier_type: "groupname" + hard_limit: 15000 + soft_limit: 15000 + grace_period: "4 03:00:00" + default_user_quota: + soft_limit: 50000 + hard_limit: 100000 + hard_limit_inodes: 10000 + default_group_quota: + soft_limit: 75000 + hard_limit: 150000 + hard_limit_inodes: 15000 + create_dir: true + state: present + register: nested_quota_result + when: can_run_tests | bool + +- name: Verify quota was created with user_quotas and group_quotas + ansible.builtin.assert: + that: + - nested_quota_result is changed + - nested_quota_result.quotas is defined + when: nested_quota_result is not skipped + +- name: Test idempotency - run same task with user_quotas and group_quotas again + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + name: "test-quota-nested" + path: "/ansible-test-quota-nested" + tenant_id: 1 + soft_limit: 100000 + hard_limit: 100000 + is_user_quota: true + enable_alarms: true + default_email: "user@example.com" + user_quotas: + - identifier: "test-user" + identifier_type: "username" + hard_limit: 15000 + soft_limit: 15000 + grace_period: "02:00:00" + email: "user1@example.com" + group_quotas: + - identifier: "test-group" + identifier_type: "groupname" + hard_limit: 15000 + soft_limit: 15000 + grace_period: "4 03:00:00" + default_user_quota: + soft_limit: 50000 + hard_limit: 100000 + hard_limit_inodes: 10000 + default_group_quota: + soft_limit: 75000 + hard_limit: 150000 + hard_limit_inodes: 15000 + state: present + register: nested_quota_idempotency_result + when: can_run_tests | bool + +- name: Verify idempotency with user_quotas and group_quotas (should not report changed) + ansible.builtin.assert: + that: + - nested_quota_idempotency_result is not changed + fail_msg: "user_quotas/group_quotas breaks idempotency - quota reported as changed when it shouldn't be" + when: nested_quota_idempotency_result is not skipped + +- name: Clean up - remove quota with user_quotas and group_quotas + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "/ansible-test-quota-nested" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up - remove test user + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "test-user" + local_provider_id: 1 + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up - remove test group + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "test-group" + local_provider_id: 1 + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Final Cleanup +# ==================== + +- name: Final cleanup - remove test quota if it exists + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove delcheck test quota if it exists + vastdata.vms.quotas: + vms: "{{ vms_auth }}" + path: "{{ test_quota_path }}-delcheck" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Skip Message +# ==================== + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping quotas integration tests - missing credentials. + + To enable integration tests, set the following in: + tests/integration/integration_config.yml + + VAST credentials: + vast_vms: "vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration quotas -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3_policy_attachment_group/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3_policy_attachment_group/defaults/main.yml new file mode 100644 index 000000000..a6c15f423 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3_policy_attachment_group/defaults/main.yml @@ -0,0 +1,31 @@ +--- +# Default variables for s3_policy_attachment_group integration tests +# Override in tests/integration/integration_config.yml +# +# Either set test_attachment_group_gid or test_attachment_groupname for an existing group, +# or leave both unset and the test will create a local group and use it. + +vast_vms: "" +vast_username: "" +vast_password: "" + +# S3 policy (created during test or use existing id) +test_s3policy_name: ansible-test-s3policy-attach-group +test_s3policy_tenant_id: 1 +test_s3policy_document: >- + {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:*"],"Resource":"*"}]} + +# Target group: optional. If none set, the test creates a local group. +test_attachment_group_gid: +test_attachment_groupname: +# Group created by the test when no attachment target is configured +test_attachment_group_name: ansible-test-s3policy-attach-group +test_created_group_gid: 19999 + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3_policy_attachment_group/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3_policy_attachment_group/tasks/main.yml new file mode 100644 index 000000000..828b8365a --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3_policy_attachment_group/tasks/main.yml @@ -0,0 +1,268 @@ +--- +# Integration tests for s3_policy_attachment_group module (Pattern E). +# When VAST credentials are available: create policy and optionally a local group, then attach/detach by gid or groupname. + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials }}" + +- name: Get default tenant for local_provider_id (when creating test group) + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "default" + state: present + register: _tenant_result + when: can_run_tests | bool + +- name: Create local group for s3_policy_attachment_group test (no target configured) + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_attachment_group_name }}" + gid: "{{ test_created_group_gid }}" + local_provider_id: "{{ _tenant_result.tenants.local_provider_id }}" + state: present + register: _create_group_result + when: + - can_run_tests | bool + - _tenant_result is not skipped + +- name: Set created-group flag and gid when we created the group + ansible.builtin.set_fact: + s3_attachment_we_created_group: true + test_attachment_group_gid: "{{ test_created_group_gid }}" + when: + - can_run_tests | bool + - _create_group_result is not skipped + +- name: Set attachment target facts for group tests + ansible.builtin.set_fact: + use_group_gid: >- + {{ (s3_attachment_we_created_group | default(false)) or + ((test_attachment_group_gid | default(none)) is not none) }} + use_groupname: >- + {{ (not (s3_attachment_we_created_group | default(false))) and + ((test_attachment_groupname | default('') or '') | length > 0) and + (test_attachment_group_gid | default(none)) is none }} + when: can_run_tests | bool + +# ==================== +# Ensure test S3 policy exists +# ==================== + +- name: Create or ensure S3 policy exists for attachment tests + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}" + tenant_id: "{{ test_s3policy_tenant_id }}" + policy: "{{ test_s3policy_document }}" + state: present + register: policy_result + when: can_run_tests | bool + +- name: Set policy ID for attachment tests + ansible.builtin.set_fact: + test_policy_id: "{{ policy_result.s3policies.id }}" + when: + - can_run_tests | bool + - policy_result is not skipped + - policy_result.s3policies is defined + +# ==================== +# Attach (group by gid or groupname) +# ==================== + +- name: Attach S3 policy to group (s3_policy_attachment_group by gid or groupname) + vastdata.vms.s3_policy_attachment_group: + vms: "{{ vms_auth }}" + s3_policy_id: "{{ test_policy_id }}" + gid: "{{ test_attachment_group_gid if use_group_gid | default(false) else omit }}" + groupname: "{{ test_attachment_groupname if use_groupname | default(false) else omit }}" + state: present + register: attach_group_result + when: + - can_run_tests | bool + - test_policy_id is defined + - use_group_gid | default(false) or use_groupname | default(false) + +- name: Verify attach to group (first attach must report changed and return summary) + ansible.builtin.assert: + that: + - attach_group_result is changed + - attach_group_result.s3_policy_attachment is defined + - attach_group_result.s3_policy_attachment.policy_id | int == test_policy_id | int + - attach_group_result.s3_policy_attachment.target_type == "group" + fail_msg: "First attach to group must report changed=true and return s3_policy_attachment summary" + when: attach_group_result is not skipped + +- name: Backend state - query group by gid (after attach) + ansible.builtin.uri: + url: "https://{{ vast_vms }}/api/latest/groups/query/?gid={{ test_attachment_group_gid }}" + method: GET + user: "{{ vast_username }}" + password: "{{ vast_password }}" + force_basic_auth: true + validate_certs: false + return_content: true + register: _group_query_after_attach_gid + when: + - attach_group_result is not skipped + - use_group_gid | default(false) | bool + +# Swagger declares GET /groups/query/ returns a single object (GroupData). +- name: Assert group query response is single object (per Swagger) + ansible.builtin.assert: + that: _group_query_after_attach_gid.json is mapping + fail_msg: "GET /groups/query/ must return a single object (GroupData). Check API/spec." + when: _group_query_after_attach_gid is not skipped + +- name: Set group entity from query response (after attach) + ansible.builtin.set_fact: + _group_entity: "{{ _group_query_after_attach_gid.json }}" + when: _group_query_after_attach_gid is not skipped + +- name: Assert group s3_policies_ids contains test policy after attach + ansible.builtin.assert: + that: + - test_policy_id | int in (_group_entity.s3_policies_ids | default([]) | map('int') | list) + fail_msg: >- + Backend state: group s3_policies_ids must contain policy {{ test_policy_id }} after attach. + Got {{ _group_entity.s3_policies_ids | default('missing') }} + when: _group_entity is defined + +# ==================== +# Idempotency - attach again (no change) +# ==================== + +- name: Idempotency - attach same policy to group again + vastdata.vms.s3_policy_attachment_group: + vms: "{{ vms_auth }}" + s3_policy_id: "{{ test_policy_id }}" + gid: "{{ test_attachment_group_gid if use_group_gid | default(false) else omit }}" + groupname: "{{ test_attachment_groupname if use_groupname | default(false) else omit }}" + state: present + ignore_present: true + register: idem_group_result + when: + - can_run_tests | bool + - test_policy_id is defined + - use_group_gid | default(false) or use_groupname | default(false) + +- name: Verify idempotency (no change on second attach) + ansible.builtin.assert: + that: + - idem_group_result is not changed + - idem_group_result.s3_policy_attachment is defined + fail_msg: "Second attach must report changed=false (idempotency)" + when: idem_group_result is not skipped + +# ==================== +# Detach +# ==================== + +- name: Detach S3 policy from group + vastdata.vms.s3_policy_attachment_group: + vms: "{{ vms_auth }}" + s3_policy_id: "{{ test_policy_id }}" + gid: "{{ test_attachment_group_gid if use_group_gid | default(false) else omit }}" + groupname: "{{ test_attachment_groupname if use_groupname | default(false) else omit }}" + state: absent + register: detach_group_result + when: + - can_run_tests | bool + - test_policy_id is defined + - use_group_gid | default(false) or use_groupname | default(false) + +- name: Verify detach (first detach must report changed) + ansible.builtin.assert: + that: + - detach_group_result is changed + - detach_group_result.s3_policy_attachment is defined + fail_msg: "First detach from group must report changed=true" + when: detach_group_result is not skipped + +- name: Backend state - query group by gid after detach + ansible.builtin.uri: + url: "https://{{ vast_vms }}/api/latest/groups/query/?gid={{ test_attachment_group_gid }}" + method: GET + user: "{{ vast_username }}" + password: "{{ vast_password }}" + force_basic_auth: true + validate_certs: false + return_content: true + register: _group_query_after_detach_gid + when: + - detach_group_result is not skipped + - use_group_gid | default(false) | bool + +# Same as after attach: Swagger says single object. +- name: Assert group query response is single object after detach (per Swagger) + ansible.builtin.assert: + that: _group_query_after_detach_gid.json is mapping + fail_msg: "GET /groups/query/ must return a single object (GroupData). Check API/spec." + when: _group_query_after_detach_gid is not skipped + +- name: Set group entity from query response (after detach) + ansible.builtin.set_fact: + _group_entity_detach: "{{ _group_query_after_detach_gid.json }}" + when: _group_query_after_detach_gid is not skipped + +- name: Assert group s3_policies_ids does not contain test policy after detach + ansible.builtin.assert: + that: + - test_policy_id | int not in (_group_entity_detach.s3_policies_ids | default([]) | map('int') | list) + fail_msg: >- + Backend state: group s3_policies_ids must not contain policy {{ test_policy_id }} after detach. + Got {{ _group_entity_detach.s3_policies_ids | default('missing') }} + when: _group_entity_detach is defined + +# ==================== +# Detach idempotency +# ==================== + +- name: Idempotency - detach again (no change) + vastdata.vms.s3_policy_attachment_group: + vms: "{{ vms_auth }}" + s3_policy_id: "{{ test_policy_id }}" + gid: "{{ test_attachment_group_gid if use_group_gid | default(false) else omit }}" + groupname: "{{ test_attachment_groupname if use_groupname | default(false) else omit }}" + state: absent + register: idem_detach_result + when: + - can_run_tests | bool + - test_policy_id is defined + - use_group_gid | default(false) or use_groupname | default(false) + +- name: Verify detach idempotency (no change on second detach) + ansible.builtin.assert: + that: + - idem_detach_result is not changed + - idem_detach_result.s3_policy_attachment is defined + fail_msg: "Second detach must report changed=false (idempotency)" + when: idem_detach_result is not skipped + +# ==================== +# Cleanup +# ==================== + +- name: Remove S3 policy created for s3_policy_attachment_group test + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Remove local group created for s3_policy_attachment_group test + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_attachment_group_name }}" + state: absent + when: s3_attachment_we_created_group | default(false) | bool diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3_policy_attachment_user/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3_policy_attachment_user/defaults/main.yml new file mode 100644 index 000000000..b63fe5a45 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3_policy_attachment_user/defaults/main.yml @@ -0,0 +1,32 @@ +--- +# Default variables for s3_policy_attachment_user integration tests +# Override in tests/integration/integration_config.yml +# +# Either set test_attachment_user_uid or test_attachment_username for an existing user, +# or leave both unset and the test will create a local user and use it. + +vast_vms: "" +vast_username: "" +vast_password: "" + +# S3 policy (created during test or use existing id) +test_s3policy_name: ansible-test-s3policy-attach-user +test_s3policy_tenant_id: 1 +test_s3policy_document: >- + {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:*"],"Resource":"*"}]} + +# Target user: optional. If none set, the test creates a local user. +test_attachment_user_uid: +test_attachment_username: +# User created by the test when no attachment target is configured +test_attachment_user_name: ansible-test-s3policy-attach-user +test_created_user_uid: 19998 +test_created_group_gid: 19999 + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3_policy_attachment_user/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3_policy_attachment_user/tasks/main.yml new file mode 100644 index 000000000..9cb8fb661 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3_policy_attachment_user/tasks/main.yml @@ -0,0 +1,305 @@ +--- +# Integration tests for s3_policy_attachment_user module (Pattern E). +# When VAST credentials are available: create policy and optionally a local user, then attach/detach by uid or username. + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials }}" + +- name: Get default tenant for local_provider_id (when creating test user) + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "default" + state: present + register: _tenant_result + when: can_run_tests | bool + +- name: Ensure test group does not exist before creating (for user's primary group) + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_attachment_user_name }}-group" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Create local group for test user (no target configured) + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_attachment_user_name }}-group" + gid: "{{ test_created_group_gid }}" + local_provider_id: "{{ _tenant_result.tenants.local_provider_id }}" + state: present + register: _create_group_result + when: + - can_run_tests | bool + - _tenant_result is not skipped + +- name: Ensure test user does not exist before creating + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_attachment_user_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Create local user for s3_policy_attachment_user test (no target configured) + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_attachment_user_name }}" + uid: "{{ test_created_user_uid }}" + gids: ["{{ test_created_group_gid }}"] + leading_gid: "{{ test_created_group_gid }}" + local_provider_id: "{{ _tenant_result.tenants.local_provider_id }}" + state: present + register: _create_user_result + when: + - can_run_tests | bool + - _create_group_result is not skipped + +- name: Set created-user flag and uid when we created the user + ansible.builtin.set_fact: + s3_attachment_we_created_user: true + test_attachment_user_uid: "{{ test_created_user_uid }}" + when: + - can_run_tests | bool + - _create_user_result is not skipped + +- name: Set attachment target facts for user tests + ansible.builtin.set_fact: + use_user_uid: >- + {{ (s3_attachment_we_created_user | default(false)) or + ((test_attachment_user_uid | default(none)) is not none) }} + use_username: >- + {{ (not (s3_attachment_we_created_user | default(false))) and + ((test_attachment_username | default('') or '') | length > 0) and + (test_attachment_user_uid | default(none)) is none }} + when: can_run_tests | bool + +# ==================== +# Ensure test S3 policy exists +# ==================== + +- name: Create or ensure S3 policy exists for attachment tests + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}" + tenant_id: "{{ test_s3policy_tenant_id }}" + policy: "{{ test_s3policy_document }}" + state: present + register: policy_result + when: can_run_tests | bool + +- name: Set policy ID for attachment tests + ansible.builtin.set_fact: + test_policy_id: "{{ policy_result.s3policies.id }}" + when: + - can_run_tests | bool + - policy_result is not skipped + - policy_result.s3policies is defined + +# ==================== +# Attach (user by uid or username) +# ==================== + +- name: Attach S3 policy to user (s3_policy_attachment_user by uid or username) + vastdata.vms.s3_policy_attachment_user: + vms: "{{ vms_auth }}" + s3_policy_id: "{{ test_policy_id }}" + uid: "{{ test_attachment_user_uid if use_user_uid | default(false) else omit }}" + username: "{{ test_attachment_username if use_username | default(false) else omit }}" + state: present + register: attach_user_result + when: + - can_run_tests | bool + - test_policy_id is defined + - use_user_uid | default(false) or use_username | default(false) + +- name: Verify attach to user (first attach must report changed and return summary) + ansible.builtin.assert: + that: + - attach_user_result is changed + - attach_user_result.s3_policy_attachment is defined + - attach_user_result.s3_policy_attachment.policy_id | int == test_policy_id | int + - attach_user_result.s3_policy_attachment.target_type == "user" + fail_msg: "First attach to user must report changed=true and return s3_policy_attachment summary" + when: attach_user_result is not skipped + +- name: Backend state - query user by uid (after attach) + ansible.builtin.uri: + url: "https://{{ vast_vms }}/api/latest/users/query/?uid={{ test_attachment_user_uid }}" + method: GET + user: "{{ vast_username }}" + password: "{{ vast_password }}" + force_basic_auth: true + validate_certs: false + return_content: true + register: _user_query_after_attach_uid + when: + - attach_user_result is not skipped + - use_user_uid | default(false) | bool + +# Swagger declares GET /users/query/ returns a single object (UserQueryData). +- name: Assert user query response is single object (per Swagger) + ansible.builtin.assert: + that: _user_query_after_attach_uid.json is mapping + fail_msg: "GET /users/query/ must return a single object (UserQueryData). Check API/spec." + when: _user_query_after_attach_uid is not skipped + +- name: Set user entity from query response (after attach) + ansible.builtin.set_fact: + _user_entity: "{{ _user_query_after_attach_uid.json }}" + when: _user_query_after_attach_uid is not skipped + +- name: Assert user s3_policies_ids contains test policy after attach + ansible.builtin.assert: + that: + - test_policy_id | int in (_user_entity.s3_policies_ids | default([]) | map('int') | list) + fail_msg: >- + Backend state: user s3_policies_ids must contain policy {{ test_policy_id }} after attach. + Got {{ _user_entity.s3_policies_ids | default('missing') }} + when: _user_entity is defined + +# ==================== +# Idempotency - attach again (no change) +# ==================== + +- name: Idempotency - attach same policy to user again + vastdata.vms.s3_policy_attachment_user: + vms: "{{ vms_auth }}" + s3_policy_id: "{{ test_policy_id }}" + uid: "{{ test_attachment_user_uid if use_user_uid | default(false) else omit }}" + username: "{{ test_attachment_username if use_username | default(false) else omit }}" + state: present + ignore_present: true + register: idem_user_result + when: + - can_run_tests | bool + - test_policy_id is defined + - use_user_uid | default(false) or use_username | default(false) + +- name: Verify user attach idempotency (no change on second attach) + ansible.builtin.assert: + that: + - idem_user_result is not changed + - idem_user_result.s3_policy_attachment is defined + fail_msg: "Second attach to user must report changed=false (idempotency)" + when: idem_user_result is not skipped + +# ==================== +# Detach +# ==================== + +- name: Detach S3 policy from user + vastdata.vms.s3_policy_attachment_user: + vms: "{{ vms_auth }}" + s3_policy_id: "{{ test_policy_id }}" + uid: "{{ test_attachment_user_uid if use_user_uid | default(false) else omit }}" + username: "{{ test_attachment_username if use_username | default(false) else omit }}" + state: absent + register: detach_user_result + when: + - can_run_tests | bool + - test_policy_id is defined + - use_user_uid | default(false) or use_username | default(false) + +- name: Verify detach from user (first detach must report changed) + ansible.builtin.assert: + that: + - detach_user_result is changed + - detach_user_result.s3_policy_attachment is defined + fail_msg: "First detach from user must report changed=true" + when: detach_user_result is not skipped + +- name: Backend state - query user by uid after detach + ansible.builtin.uri: + url: "https://{{ vast_vms }}/api/latest/users/query/?uid={{ test_attachment_user_uid }}" + method: GET + user: "{{ vast_username }}" + password: "{{ vast_password }}" + force_basic_auth: true + validate_certs: false + return_content: true + register: _user_query_after_detach_uid + when: + - detach_user_result is not skipped + - use_user_uid | default(false) | bool + +# Same as after attach: Swagger says single object. +- name: Assert user query response is single object after detach (per Swagger) + ansible.builtin.assert: + that: _user_query_after_detach_uid.json is mapping + fail_msg: "GET /users/query/ must return a single object (UserQueryData). Check API/spec." + when: _user_query_after_detach_uid is not skipped + +- name: Set user entity from query response (after detach) + ansible.builtin.set_fact: + _user_entity_detach: "{{ _user_query_after_detach_uid.json }}" + when: _user_query_after_detach_uid is not skipped + +- name: Assert user s3_policies_ids does not contain test policy after detach + ansible.builtin.assert: + that: + - test_policy_id | int not in (_user_entity_detach.s3_policies_ids | default([]) | map('int') | list) + fail_msg: >- + Backend state: user s3_policies_ids must not contain policy {{ test_policy_id }} after detach. + Got {{ _user_entity_detach.s3_policies_ids | default('missing') }} + when: _user_entity_detach is defined + +# ==================== +# Detach idempotency +# ==================== + +- name: Idempotency - detach from user again (no change) + vastdata.vms.s3_policy_attachment_user: + vms: "{{ vms_auth }}" + s3_policy_id: "{{ test_policy_id }}" + uid: "{{ test_attachment_user_uid if use_user_uid | default(false) else omit }}" + username: "{{ test_attachment_username if use_username | default(false) else omit }}" + state: absent + register: idem_detach_user_result + when: + - can_run_tests | bool + - test_policy_id is defined + - use_user_uid | default(false) or use_username | default(false) + +- name: Verify user detach idempotency (no change on second detach) + ansible.builtin.assert: + that: + - idem_detach_user_result is not changed + - idem_detach_user_result.s3_policy_attachment is defined + fail_msg: "Second detach from user must report changed=false (idempotency)" + when: idem_detach_user_result is not skipped + +# ==================== +# Cleanup +# ==================== + +- name: Remove S3 policy created for s3_policy_attachment_user test + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Remove local user created for s3_policy_attachment_user test + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_attachment_user_name }}" + state: absent + when: s3_attachment_we_created_user | default(false) | bool + +- name: Remove local group created for s3_policy_attachment_user test + vastdata.vms.groups: + vms: "{{ vms_auth }}" + name: "{{ test_attachment_user_name }}-group" + state: absent + when: s3_attachment_we_created_user | default(false) | bool diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3policies/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3policies/defaults/main.yml new file mode 100644 index 000000000..a513b5cea --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3policies/defaults/main.yml @@ -0,0 +1,23 @@ +--- +# Default variables for s3policies integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +# Leave empty to skip tests gracefully +vast_vms: "" +vast_username: "" +vast_password: "" + +# Test S3 policy configuration +test_s3policy_name: ansible-test-s3policy +test_s3policy_tenant_id: 1 +test_s3policy_document: >- + {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:*"],"Resource":"*"}]} + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3policies/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3policies/tasks/main.yml new file mode 100644 index 000000000..3c27fd184 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/s3policies/tasks/main.yml @@ -0,0 +1,358 @@ +--- +# Integration tests for s3policies module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials }}" + +# ==================== +# Cleanup - Remove existing test S3 policy if present +# ==================== + +- name: Clean up - ensure test S3 policy doesn't exist before starting + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}" + tenant_id: "{{ test_s3policy_tenant_id }}" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +- name: Clean up - ensure test S3 policy -updated doesn't exist either + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}-updated" + tenant_id: "{{ test_s3policy_tenant_id }}" + state: absent + register: cleanup_result2 + when: can_run_tests | bool + failed_when: false + +# ==================== +# Create Tests +# ==================== + +- name: Test creating an S3 policy + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}" + tenant_id: "{{ test_s3policy_tenant_id }}" + policy: "{{ test_s3policy_document }}" + state: present + register: create_result + when: can_run_tests | bool + +- name: Verify S3 policy was created + ansible.builtin.assert: + that: + - create_result is changed + - create_result.s3policies is defined + - create_result.s3policies.name == test_s3policy_name + # Note: enabled field defaults to true when not specified during creation + - create_result.s3policies.enabled == true + - create_result.diff is defined + - create_result.diff.before == {} + - create_result.diff.after is defined + when: create_result is not skipped + +# ==================== +# Idempotency Tests - Create +# ==================== + +- name: Test idempotency - create same S3 policy again + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}" + tenant_id: "{{ test_s3policy_tenant_id }}" + policy: "{{ test_s3policy_document }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify idempotency (should not change) + ansible.builtin.assert: + that: + - result is not changed + - result.s3policies is defined + when: result is not skipped + +# ==================== +# Update Tests - Change name and enable/disable +# ==================== + +- name: "Test update - change name and disable policy" + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + id: "{{ create_result.s3policies.id }}" # Use ID for lookup to enable rename + name: "{{ test_s3policy_name }}-updated" + tenant_id: "{{ test_s3policy_tenant_id }}" + policy: "{{ test_s3policy_document }}" + enabled: false + state: present + register: update_result + when: can_run_tests | bool + +- name: Verify S3 policy name was updated and enabled was set to false + ansible.builtin.assert: + that: + - update_result is changed + - update_result.s3policies is defined + - update_result.s3policies.name == test_s3policy_name + '-updated' + - update_result.s3policies.enabled == false + when: update_result is not skipped + +# ==================== +# Idempotency Tests - After Update +# ==================== + +- name: Test idempotency after update + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}-updated" + tenant_id: "{{ test_s3policy_tenant_id }}" + policy: "{{ test_s3policy_document }}" + enabled: false + state: present + register: result + when: can_run_tests | bool + +- name: Verify idempotency after update + ansible.builtin.assert: + that: + - result is not changed + - result.s3policies is defined + when: result is not skipped + +# ==================== +# Check Mode Tests - Create +# ==================== + +- name: Test check mode - create new S3 policy + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}-checkmode" + tenant_id: "{{ test_s3policy_tenant_id }}" + policy: "{{ test_s3policy_document }}" + state: present + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode create reported changes + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually create the S3 policy + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}-checkmode" + tenant_id: "{{ test_s3policy_tenant_id }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify S3 policy was never created + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Delete Tests +# ==================== + +- name: Test deleting S3 policy (using updated name) + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}-updated" + tenant_id: "{{ test_s3policy_tenant_id }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify S3 policy was deleted + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after == {} + when: result is not skipped + +# ==================== +# Idempotency Tests - Delete +# ==================== + +- name: Test delete idempotency - delete again + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}-updated" + tenant_id: "{{ test_s3policy_tenant_id }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Check Mode Delete Test +# ==================== + +- name: Create S3 policy for check mode delete test + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}-delcheck" + tenant_id: "{{ test_s3policy_tenant_id }}" + policy: "{{ test_s3policy_document }}" + state: present + register: result + when: can_run_tests | bool + +- name: Test check mode - delete + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}-delcheck" + tenant_id: "{{ test_s3policy_tenant_id }}" + state: absent + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported deletion + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually delete + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}-delcheck" + tenant_id: "{{ test_s3policy_tenant_id }}" + state: present + register: result + when: can_run_tests | bool + +- name: Check that S3 policy still exists + ansible.builtin.assert: + that: + - result is not changed + - result.s3policies is defined + when: result is not skipped + +- name: Clean up check mode delete test S3 policy + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}-delcheck" + tenant_id: "{{ test_s3policy_tenant_id }}" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Diff Mode Tests +# ==================== + +- name: Create S3 policy for diff mode test + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}-diff" + tenant_id: "{{ test_s3policy_tenant_id }}" + policy: "{{ test_s3policy_document }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify create diff output + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before == {} + - result.diff.after is defined + when: result is not skipped + +- name: Delete S3 policy with diff mode + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}-diff" + tenant_id: "{{ test_s3policy_tenant_id }}" + state: absent + diff: true + register: result + when: can_run_tests | bool + +- name: Verify delete diff output + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after == {} + when: result is not skipped + +# ==================== +# Cleanup +# ==================== + +- name: Final cleanup - remove test S3 policy if it exists (original name) + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}" + tenant_id: "{{ test_s3policy_tenant_id }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove test S3 policy if it exists (updated name) + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}-updated" + tenant_id: "{{ test_s3policy_tenant_id }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove diff test S3 policy if it exists + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_s3policy_name }}-diff" + tenant_id: "{{ test_s3policy_tenant_id }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping s3policies integration tests - credentials not configured. + + To enable integration tests, set credentials in: + tests/integration/integration_config.yml + + Example: + vast_vms: "vast-vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration s3policies -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/snapshots/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/snapshots/defaults/main.yml new file mode 100644 index 000000000..f5af819b0 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/snapshots/defaults/main.yml @@ -0,0 +1,23 @@ +--- +# Default variables for snapshots integration tests +vast_vms: "" +vast_username: "" +vast_password: "" + +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true + +# Test snapshot configuration +test_snapshot_name: "ansible-test-snapshot" +test_snapshot_name_updated: "ansible-test-snapshot-updated" + +# Test view for snapshot source +test_view_path: "/ansible-test-snapshot-view" +test_view_policy_id: 1 + +# Expiration time for testing (ISO 8601 format) +test_expiration_time: "2026-12-31T23:59:59Z" diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/snapshots/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/snapshots/tasks/main.yml new file mode 100644 index 000000000..157dab943 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/snapshots/tasks/main.yml @@ -0,0 +1,538 @@ +--- +# Integration tests for snapshots module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials }}" + +- name: Generate unique snapshot names for this test run + ansible.builtin.set_fact: + test_snapshot_name_unique: "{{ test_snapshot_name }}-{{ ansible_date_time.epoch }}" + test_snapshot_name_updated_unique: "{{ test_snapshot_name_updated }}-{{ ansible_date_time.epoch }}" + when: can_run_tests | bool + +# ==================== +# Setup - Create Prerequisites +# ==================== + +- name: Get default tenant for testing + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "default" + state: present + register: tenant_result + when: can_run_tests | bool + failed_when: false + +- name: Set default tenant ID from result + ansible.builtin.set_fact: + test_tenant_id: "{{ tenant_result.tenants.id }}" + when: can_run_tests | bool and tenant_result.tenants is defined + +- name: Create test view for snapshot source + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + policy_id: "{{ test_view_policy_id }}" + protocols: ["NFS"] + create_dir: true + state: present + register: test_view + when: can_run_tests | bool + failed_when: false + +- name: Get view ID for snapshot creation + ansible.builtin.set_fact: + test_view_id: "{{ test_view.views.id }}" + when: test_view is not skipped and test_view.views is defined + +# ==================== +# Cleanup - Remove existing test snapshots if present +# ==================== + +- name: Clean up - ensure test snapshot doesn't exist before starting + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +- name: Clean up - ensure updated test snapshot doesn't exist + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_updated_unique }}" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +- name: Clean up - ensure delcheck test snapshot doesn't exist + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}-delcheck" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +- name: Wait for cleanup to complete (async delete) + ansible.builtin.pause: + seconds: 3 + when: can_run_tests | bool and cleanup_result is changed + +# ==================== +# Create Tests +# ==================== + +- name: Test creating a snapshot + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}" + path: "{{ test_view_path }}" + state: present + register: result + when: can_run_tests | bool and test_view_id is defined + +- name: Verify snapshot was created + ansible.builtin.assert: + that: + - result is changed + - result.snapshots is defined + - result.snapshots.name == test_snapshot_name_unique + - result.snapshots.id is defined + - result.diff is defined + - result.diff.before == {} + - result.diff.after is defined + when: result is not skipped + +- name: Save snapshot ID for later tests + ansible.builtin.set_fact: + test_snapshot_id: "{{ result.snapshots.id }}" + when: result is not skipped + +# ==================== +# Idempotency Tests - Create +# ==================== + +- name: Test idempotency - create same snapshot again + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}" + path: "{{ test_view_path }}" + state: present + register: result + when: can_run_tests | bool and test_snapshot_id is defined + +- name: Verify idempotency (should not change) + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Update Tests - Expiration Time +# ==================== + +- name: Test updating snapshot - set expiration time + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}" + expiration_time: "{{ test_expiration_time }}" + state: present + register: result + when: can_run_tests | bool and test_snapshot_id is defined + +- name: Verify snapshot expiration time was updated + ansible.builtin.assert: + that: + - result is changed + - result.snapshots is defined + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: result is not skipped + +- name: Test update idempotency - expiration time + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}" + expiration_time: "{{ test_expiration_time }}" + state: present + register: result + when: can_run_tests | bool and test_snapshot_id is defined + +- name: Verify update idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Update Tests - Name Update +# ==================== + +- name: Test updating snapshot - change name + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_updated_unique }}" + path: "{{ test_view_path }}" + state: present + register: result + when: can_run_tests | bool and test_view_id is defined + +- name: Verify snapshot name update (creates new snapshot) + ansible.builtin.assert: + that: + - result is changed + - result.snapshots is defined + - result.snapshots.name == test_snapshot_name_updated_unique + when: result is not skipped + +- name: Test update idempotency - name + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_updated_unique }}" + path: "{{ test_view_path }}" + state: present + register: result + when: can_run_tests | bool and test_view_id is defined + +- name: Verify update idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +- name: Clean up updated snapshot + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_updated_unique }}" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Lookup by ID Tests +# ==================== + +- name: Test lookup by ID + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + id: "{{ test_snapshot_id }}" + state: present + register: result + when: can_run_tests | bool and test_snapshot_id is defined + +- name: Verify lookup by ID works + ansible.builtin.assert: + that: + - result is not changed + - result.snapshots is defined + - result.snapshots.id == test_snapshot_id + - result.snapshots.name == test_snapshot_name_unique + when: result is not skipped + +# ==================== +# Check Mode Tests - Create +# ==================== + +- name: Test check mode - create new snapshot + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}-checkmode" + path: "{{ test_view_path }}" + state: present + check_mode: true + register: result + when: can_run_tests | bool and test_view_id is defined + +- name: Verify check mode create reported changes + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually create the snapshot + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}-checkmode" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify snapshot was never created + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Check Mode Tests - Update +# ==================== + +- name: Test check mode - update existing snapshot expiration + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}" + expiration_time: "2027-12-31T23:59:59Z" + state: present + check_mode: true + register: result + when: can_run_tests | bool and test_snapshot_id is defined + +- name: Verify check mode update reported changes + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually update + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}" + state: present + register: result + when: can_run_tests | bool and test_snapshot_id is defined + +- name: Verify snapshot expiration was not updated by check mode + ansible.builtin.assert: + that: + - result.snapshots.expiration_time == test_expiration_time + when: result is not skipped + +# ==================== +# Check Mode Tests - Lookup +# ==================== + +- name: Test check mode - lookup existing resource + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}" + state: present + check_mode: true + register: result + when: can_run_tests | bool and test_snapshot_id is defined + +- name: Verify check mode lookup works + ansible.builtin.assert: + that: + - result is not changed + - result.snapshots is defined + when: result is not skipped + +# ==================== +# Delete Tests +# ==================== + +- name: Test deleting snapshot + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}" + state: absent + register: result + when: can_run_tests | bool and test_snapshot_id is defined + +- name: Verify snapshot was deleted + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after == {} + when: result is not skipped + +# ==================== +# Idempotency Tests - Delete +# ==================== + +- name: Test delete idempotency - delete again + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Check Mode Delete Test +# ==================== + +- name: Create snapshot for check mode delete test + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}-delcheck" + path: "{{ test_view_path }}" + state: present + register: result + when: can_run_tests | bool and test_view_id is defined + +- name: Test check mode - delete + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}-delcheck" + state: absent + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported deletion + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually delete + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}-delcheck" + state: present + register: result + when: can_run_tests | bool + +- name: Check that snapshot still exists + ansible.builtin.assert: + that: + - result is not changed + - result.snapshots is defined + when: result is not skipped + +- name: Clean up check mode delete test snapshot + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}-delcheck" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Error Handling Tests +# ==================== + +- name: Test error handling - missing name and ID + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + state: present + register: result + when: can_run_tests | bool + ignore_errors: true + +- name: Verify error was caught + ansible.builtin.assert: + that: + - result is failed + when: result is not skipped + +- name: Test error handling - invalid state + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}" + state: invalid + register: result + when: can_run_tests | bool + ignore_errors: true + +- name: Verify error was caught + ansible.builtin.assert: + that: + - result is failed + when: result is not skipped + +- name: Test error handling - missing path on create + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}-nopath" + state: present + register: result + when: can_run_tests | bool + ignore_errors: true + +- name: Verify error was caught for missing path + ansible.builtin.assert: + that: + - result is failed + when: result is not skipped + +# ==================== +# Cleanup Prerequisites +# ==================== + +- name: Clean up test view + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Final Cleanup +# ==================== + +- name: Final cleanup - remove test snapshot if it exists + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove updated test snapshot if it exists + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_updated_unique }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove delcheck test snapshot if it exists + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}-delcheck" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove checkmode test snapshot if it exists + vastdata.vms.snapshots: + vms: "{{ vms_auth }}" + name: "{{ test_snapshot_name_unique }}-checkmode" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping snapshots integration tests - credentials not configured. + + To enable integration tests, set credentials in: + tests/integration/integration_config.yml + + Example: + vast_vms: "vast-vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration snapshots -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/tenants/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/tenants/defaults/main.yml new file mode 100644 index 000000000..f338f3b7f --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/tenants/defaults/main.yml @@ -0,0 +1,29 @@ +--- +# Default variables for tenants integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +# Leave empty to skip tests gracefully +vast_vms: "" +vast_username: "" +vast_password: "" + +# Tenant test configuration +test_tenant_name: "ansible-test-tenant" +test_tenant_name_updated_alias: "/ansible-test-alias" +test_tenant_max_views: 10 +test_tenant_max_views_updated: 20 +test_tenant_smb_encryption_state: "OFF" +test_tenant_smb_encryption_state_updated: "AVAILABLE" + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true + +test_tenant_name_forcedel: "ansible-test-tenant-forcedel" +test_tenant_forcedel_view_path: "/ansible-test-tenant-forcedel-view" +test_tenant_forcedel_policy_name: "ansible-test-tenant-forcedel-policy" diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/tenants/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/tenants/tasks/main.yml new file mode 100644 index 000000000..200734d73 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/tenants/tasks/main.yml @@ -0,0 +1,582 @@ +--- +# Integration tests for tenants module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: >- + {{ (vast_vms | default('') | length > 0) + and (vast_username | default('') | length > 0) + and (vast_password | default('') | length > 0) }} + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials | bool }}" + +- name: Get default tenant to obtain local_provider_id + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "default" + state: present + register: tenant_result + when: can_run_tests | bool + +- name: Set local_provider_id from default tenant + ansible.builtin.set_fact: + test_local_provider_id: "{{ tenant_result.tenants.local_provider_id }}" + when: can_run_tests | bool + +# ==================== +# Cleanup - Ensure clean state +# ==================== + +- name: Clean up - ensure test tenant doesn't exist before starting + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +# ============================================================ +# CREATE TESTS +# ============================================================ + +- name: Test creating a tenant + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}" + local_provider_id: "{{ test_local_provider_id }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify tenant was created + ansible.builtin.assert: + that: + - result is changed + - result.tenants is defined + - result.tenants.name == test_tenant_name + - result.diff is defined + - result.diff.before == {} + - result.diff.after is defined + when: can_run_tests | bool + +# ============================================================ +# IDEMPOTENCY TESTS - CREATE +# ============================================================ + +- name: Test idempotency - create same tenant again + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}" + local_provider_id: "{{ test_local_provider_id }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify idempotency (should not change) + ansible.builtin.assert: + that: + - result is not changed + - result.tenants is defined + when: can_run_tests | bool + +# ============================================================ +# UPDATE TESTS +# ============================================================ + +- name: Test updating tenant - change max_views + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}" + max_views: "{{ test_tenant_max_views }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify tenant was updated + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: can_run_tests | bool + +- name: Test update idempotency - same max_views again + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}" + max_views: "{{ test_tenant_max_views }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify update idempotency + ansible.builtin.assert: + that: + - result is not changed + when: can_run_tests | bool + +- name: Test updating tenant - change smb_encryption_state + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}" + smb_encryption_state: "{{ test_tenant_smb_encryption_state_updated }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify smb_encryption_state was updated + ansible.builtin.assert: + that: + - result is changed + when: can_run_tests | bool + +- name: Test updating multiple fields at once + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}" + max_views: "{{ test_tenant_max_views_updated }}" + smb_encryption_state: "{{ test_tenant_smb_encryption_state }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify multi-field update + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + when: can_run_tests | bool + +# ============================================================ +# CHECK MODE TESTS - UPDATE +# ============================================================ + +# Set a known baseline before check-mode tests +- name: Set baseline for check mode test + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}" + max_views: "{{ test_tenant_max_views_updated }}" + smb_encryption_state: "{{ test_tenant_smb_encryption_state }}" + state: present + register: baseline_result + when: can_run_tests | bool + +- name: Test check mode - update max_views + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}" + max_views: 999 + state: present + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported change + ansible.builtin.assert: + that: + - result is changed + - result.tenants is defined + when: can_run_tests | bool + +- name: Verify check mode didn't actually change anything + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}" + max_views: "{{ test_tenant_max_views_updated }}" + state: present + register: result + when: can_run_tests | bool + +- name: Confirm max_views was not changed by check mode + ansible.builtin.assert: + that: + - result is not changed + when: can_run_tests | bool + +# ============================================================ +# CHECK MODE TESTS - CREATE +# ============================================================ + +- name: Test check mode - create new tenant + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}-checkmode" + state: present + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode create reported change + ansible.builtin.assert: + that: + - result is changed + when: can_run_tests | bool + +- name: Verify check mode didn't actually create the tenant + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}-checkmode" + state: absent + register: result + when: can_run_tests | bool + +- name: Confirm tenant was never created + ansible.builtin.assert: + that: + - result is not changed + when: can_run_tests | bool + +# ============================================================ +# DIFF MODE TESTS +# ============================================================ + +- name: Test diff mode - update max_views + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}" + max_views: "{{ test_tenant_max_views }}" + state: present + diff: true + register: result + when: can_run_tests | bool + +- name: Verify diff mode returned diff data + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: can_run_tests | bool + +# ============================================================ +# DELETE TESTS +# ============================================================ + +- name: Test deleting tenant + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify tenant was deleted + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after == {} + when: can_run_tests | bool + +# ============================================================ +# IDEMPOTENCY TESTS - DELETE +# ============================================================ + +- name: Test delete idempotency - delete again + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed + when: can_run_tests | bool + +# ============================================================ +# CHECK MODE TESTS - DELETE +# ============================================================ + +- name: Create tenant for check mode delete test + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}-delcheck" + local_provider_id: "{{ test_local_provider_id }}" + state: present + register: result + when: can_run_tests | bool + +- name: Test check mode - delete + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}-delcheck" + state: absent + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported deletion + ansible.builtin.assert: + that: + - result is changed + when: can_run_tests | bool + +- name: Verify check mode didn't actually delete + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}-delcheck" + state: present + register: result + when: can_run_tests | bool + +- name: Confirm tenant still exists + ansible.builtin.assert: + that: + - result is not changed + - result.tenants is defined + when: can_run_tests | bool + +- name: Clean up check mode delete test tenant + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}-delcheck" + state: absent + when: can_run_tests | bool + failed_when: false + +# ============================================================ +# FORCE_REMOVE TESTS +# ============================================================ +- name: Force-remove pre-cleanup - delete leftover view (if any) + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_tenant_forcedel_view_path }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Force-remove pre-cleanup - delete leftover view policy (if any) + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_forcedel_policy_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Force-remove pre-cleanup - delete leftover tenant (if any) + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name_forcedel }}" + state: absent + force_remove: true + when: can_run_tests | bool + failed_when: false + +- name: Force-remove pre-cleanup - delete orphan local provider (if any) + vastdata.vms.localproviders: + vms: "{{ vms_auth }}" + name: "provider-{{ test_tenant_name_forcedel }}" + state: absent + when: can_run_tests | bool + failed_when: false + +# --- Set up: tenant -> view policy -> view (makes the tenant non-empty) --- + +- name: Force-remove - create dedicated tenant (auto-creates local provider) + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name_forcedel }}" + state: present + register: forcedel_tenant + when: can_run_tests | bool + +- name: Force-remove - verify tenant was created + ansible.builtin.assert: + that: + - forcedel_tenant is changed + - forcedel_tenant.tenants is defined + - forcedel_tenant.tenants.id is defined + when: forcedel_tenant is not skipped + +- name: Force-remove - create view policy bound to the tenant + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_forcedel_policy_name }}" + flavor: "NFS" + tenant_id: "{{ forcedel_tenant.tenants.id }}" + state: present + register: forcedel_policy + when: can_run_tests | bool and forcedel_tenant is not skipped + +- name: Force-remove - create view inside the tenant (makes it non-empty) + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_tenant_forcedel_view_path }}" + policy_id: "{{ forcedel_policy.viewpolicies.id }}" + tenant_id: "{{ forcedel_tenant.tenants.id }}" + protocols: ["NFS"] + create_dir: true + state: present + when: can_run_tests | bool and forcedel_tenant is not skipped + +# --- Test 1: delete WITHOUT force_remove must fail --- + +- name: Force-remove - try to delete non-empty tenant WITHOUT force_remove + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name_forcedel }}" + state: absent + register: no_force_result + when: can_run_tests | bool and forcedel_tenant is not skipped + ignore_errors: true + +- name: Verify deletion without force_remove was rejected by VAST + ansible.builtin.assert: + that: + - no_force_result is failed + - "'DELETE ' in (no_force_result.msg | default(''))" + - "'/tenants/' in (no_force_result.msg | default(''))" + - "' -> 400: ' in (no_force_result.msg | default(''))" + - "'bad_request' in (no_force_result.msg | default('') | lower)" + - "'related objects' in (no_force_result.msg | default('') | lower)" + when: no_force_result is not skipped + +# --- Test 2: delete WITH force_remove must succeed --- + +- name: Force-remove - delete non-empty tenant WITH force_remove=true + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name_forcedel }}" + state: absent + force_remove: true + register: force_result + when: can_run_tests | bool and forcedel_tenant is not skipped + +- name: Verify deletion with force_remove=true succeeded + ansible.builtin.assert: + that: + - force_result is changed + - force_result.diff is defined + - force_result.diff.after == {} + when: force_result is not skipped + +# --- Test 3: confirm the tenant is truly absent afterwards --- + +- name: Force-remove - look up tenant after force_remove (check_mode) + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name_forcedel }}" + state: present + check_mode: true + register: forcedel_after + when: can_run_tests | bool + failed_when: false + +- name: Verify tenant is truly absent after force_remove + ansible.builtin.assert: + that: + - (forcedel_after is failed) or (forcedel_after.tenants is not defined) or (forcedel_after.tenants in [None, {}]) or (forcedel_after.tenants.id is not + defined) + when: forcedel_after is not skipped + +# --- Cleanup: drop the auto-created local provider so re-runs are clean --- + +- name: Force-remove - cleanup auto-created local provider + vastdata.vms.localproviders: + vms: "{{ vms_auth }}" + name: "provider-{{ test_tenant_name_forcedel }}" + state: absent + when: can_run_tests | bool + failed_when: false + +# ============================================================ +# FINAL CLEANUP +# ============================================================ + +- name: Final cleanup - remove test tenant if it exists + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove delcheck test tenant if it exists + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}-delcheck" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove checkmode test tenant if it exists + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name }}-checkmode" + state: absent + when: can_run_tests | bool + failed_when: false + +# Safety-net cleanups for the force_remove test resources, in case the +# test block itself failed mid-flight and left state behind. Order +# matters: view -> view policy -> tenant (force_remove) -> local provider. +- name: Final cleanup - remove force_remove test view if it exists + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_tenant_forcedel_view_path }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove force_remove test view policy if it exists + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_forcedel_policy_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - force_remove test tenant if it exists + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ test_tenant_name_forcedel }}" + state: absent + force_remove: true + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove force_remove test tenant's auto-created local provider + vastdata.vms.localproviders: + vms: "{{ vms_auth }}" + name: "provider-{{ test_tenant_name_forcedel }}" + state: absent + when: can_run_tests | bool + failed_when: false + +# ============================================================ +# SKIP MESSAGE +# ============================================================ + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping tenants integration tests - missing credentials. + + To enable integration tests, set the following in: + tests/integration/integration_config.yml + + VAST credentials: + vast_vms: "vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration tenants -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/user_key/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/user_key/defaults/main.yml new file mode 100644 index 000000000..3c86983b2 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/user_key/defaults/main.yml @@ -0,0 +1,27 @@ +--- +# Default variables for user_key integration tests +# Override in tests/integration/integration_config.yml + +vast_vms: "" +vast_username: "" +vast_password: "" + +# Test user configuration +test_user_name: ansible-test-user-key +test_user_uid: 19997 +test_user_gid: 19997 + +# Tenant for access key generation +test_tenant_id: 1 + +test_regression_tenant_name: ansible-test-user-key-tenant +test_regression_user_name: ansible-test-user-key-regression-user +test_regression_user_uid: 19998 + +# Reusable connection dict +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/user_key/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/user_key/tasks/main.yml new file mode 100644 index 000000000..8c8330152 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/user_key/tasks/main.yml @@ -0,0 +1,759 @@ +--- +# Integration tests for user_key module +# Tests S3 access key management for users + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials }}" + +- name: Get default tenant to obtain local_provider_id + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "default" + state: present + register: tenant_result + when: can_run_tests | bool + +- name: Set local_provider_id from default tenant + ansible.builtin.set_fact: + test_local_provider_id: "{{ tenant_result.tenants.local_provider_id }}" + when: can_run_tests | bool + +# ==================== +# Cleanup - Ensure clean state +# ==================== + +- name: Clean up - ensure test user doesn't exist before starting + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +# ==================== +# Create Test User +# ==================== + +- name: Create test user for access key tests + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + uid: "{{ test_user_uid }}" + gids: ["{{ test_user_gid }}"] + leading_gid: "{{ test_user_gid }}" + local_provider_id: "{{ test_local_provider_id }}" + state: present + register: user_result + when: can_run_tests | bool + +- name: Set test user ID + ansible.builtin.set_fact: + test_user_id: "{{ user_result.users.id }}" + when: can_run_tests | bool + +# ==================== +# Create Access Key Tests +# ==================== + +- name: Test creating access key for user + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ test_user_id }}" + tenant_id: "{{ test_tenant_id }}" + state: present + register: create_result + when: can_run_tests | bool + +- name: Verify access key was created + ansible.builtin.assert: + that: + - create_result is changed + - create_result.result is defined + - create_result.result.access_key is defined + - create_result.result.secret_key is defined + when: create_result is not skipped + +- name: Save access key for later tests + ansible.builtin.set_fact: + test_access_key: "{{ create_result.result.access_key }}" + test_secret_key: "{{ create_result.result.secret_key }}" + when: create_result is not skipped + +# ==================== +# Verify Backend State After Create +# ==================== + +- name: Query user to verify access key exists + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + state: present + register: user_after_create + when: can_run_tests | bool + +- name: Assert access_keys list is not empty + ansible.builtin.assert: + that: + - user_after_create.users.access_keys is defined + - user_after_create.users.access_keys | length > 0 + - test_access_key in (user_after_create.users.access_keys | map(attribute='access_key') | list) + fail_msg: "Backend state: user must have the created access key" + when: user_after_create is not skipped + +# ==================== +# Create Second Access Key (Multiple Keys) +# ==================== + +- name: Create second access key for same user + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ test_user_id }}" + tenant_id: "{{ test_tenant_id }}" + state: present + register: create_result2 + when: can_run_tests | bool + +- name: Verify second access key was created + ansible.builtin.assert: + that: + - create_result2 is changed + - create_result2.result is defined + - create_result2.result.access_key is defined + - create_result2.result.access_key != test_access_key + fail_msg: "Second access key should be different from the first" + when: create_result2 is not skipped + +- name: Save second access key + ansible.builtin.set_fact: + test_access_key2: "{{ create_result2.result.access_key }}" + when: create_result2 is not skipped + +# ==================== +# Update Existing Key +# ==================== + +- name: "Verify first key is enabled after creation" + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + state: present + register: user_before_disable + when: can_run_tests | bool + +- name: "Confirm first key is currently enabled" + ansible.builtin.assert: + that: + - user_before_disable.users.access_keys | selectattr('access_key', 'equalto', test_access_key) | map(attribute='enabled') | first + fail_msg: "First access key should be enabled before disable test" + when: user_before_disable is not skipped + +- name: "Disable existing access key" + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ test_user_id }}" + access_key: "{{ test_access_key }}" + tenant_id: "{{ test_tenant_id }}" + enabled: false + state: present + register: disable_key_result + when: can_run_tests | bool + +- name: "Verify disable operation succeeded and reported change" + ansible.builtin.assert: + that: + - disable_key_result is changed + - disable_key_result.result is defined + - disable_key_result.result.enabled == false + - disable_key_result.result.tenant_id == test_tenant_id + when: disable_key_result is not skipped + +- name: "Verify backend state - key should be disabled" + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + state: present + register: user_after_disable + when: can_run_tests | bool + +- name: "Confirm key is actually disabled in backend" + ansible.builtin.assert: + that: + - user_after_disable.users.access_keys | selectattr('access_key', 'equalto', test_access_key) | map(attribute='enabled') | first == false + fail_msg: "Backend verification failed: access key should be disabled" + when: user_after_disable is not skipped + +# ==================== +# Idempotency Tests - Disable +# ==================== + +- name: "IDEMPOTENCY: Disable same key again (should report no change)" + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ test_user_id }}" + access_key: "{{ test_access_key }}" + tenant_id: "{{ test_tenant_id }}" + enabled: false + state: present + register: disable_idempotent_result + when: can_run_tests | bool + +- name: "IDEMPOTENCY: Verify second disable reported no change" + ansible.builtin.assert: + that: + - disable_idempotent_result is not changed + - disable_idempotent_result.result.enabled == false + fail_msg: | + IDEMPOTENCY: Second disable operation reported changed=true. + This indicates the module is not properly detecting existing state. + Expected: changed=false + Actual: changed={{ disable_idempotent_result.changed }} + success_msg: "Idempotency verified: repeated disable correctly reported changed=false" + when: disable_idempotent_result is not skipped + +# ==================== +# Re-enable Tests (Toggle enabled state) +# ==================== + +- name: "Re-enable previously disabled key" + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ test_user_id }}" + access_key: "{{ test_access_key }}" + tenant_id: "{{ test_tenant_id }}" + enabled: true + state: present + register: enable_key_result + when: can_run_tests | bool + +- name: "Verify re-enable operation succeeded" + ansible.builtin.assert: + that: + - enable_key_result is changed + - enable_key_result.result is defined + - enable_key_result.result.enabled == true + - enable_key_result.result.tenant_id == test_tenant_id + fail_msg: "Re-enable operation failed or didn't report correct state" + when: enable_key_result is not skipped + +- name: "Verify backend state - key should be enabled" + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + state: present + register: user_after_enable + when: can_run_tests | bool + +- name: "Confirm key is actually enabled in backend" + ansible.builtin.assert: + that: + - user_after_enable.users.access_keys | selectattr('access_key', 'equalto', test_access_key) | map(attribute='enabled') | first == true + fail_msg: "Backend verification failed: access key should be enabled" + when: user_after_enable is not skipped + +# ==================== +# Idempotency Tests - Enable +# ==================== + +- name: "IDEMPOTENCY: Enable same key again (should report no change)" + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ test_user_id }}" + access_key: "{{ test_access_key }}" + tenant_id: "{{ test_tenant_id }}" + enabled: true + state: present + register: enable_idempotent_result + when: can_run_tests | bool + +- name: "IDEMPOTENCY: Verify second enable reported no change" + ansible.builtin.assert: + that: + - enable_idempotent_result is not changed + - enable_idempotent_result.result.enabled == true + fail_msg: | + IDEMPOTENCY: Second enable operation reported changed=true. + Expected: changed=false + Actual: changed={{ enable_idempotent_result.changed }} + success_msg: "Idempotency verified: repeated enable correctly reported changed=false" + when: enable_idempotent_result is not skipped + +# ==================== +# Check Mode Tests - Update (Disable/Enable) +# ==================== + +- name: "Test check mode - disable key (should report change but not modify)" + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ test_user_id }}" + access_key: "{{ test_access_key }}" + tenant_id: "{{ test_tenant_id }}" + enabled: false + state: present + check_mode: true + register: check_disable_result + when: can_run_tests | bool + +- name: "Verify check mode reported change for disable" + ansible.builtin.assert: + that: + - check_disable_result is changed + - check_disable_result.result is defined + - check_disable_result.diff is defined + - check_disable_result.diff.before.enabled == true + - check_disable_result.diff.after.enabled == false + fail_msg: "Check mode should report change with proper diff" + when: check_disable_result is not skipped + +- name: "Verify check mode didn't actually modify the key" + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + state: present + register: user_after_check_disable + when: can_run_tests | bool + +- name: "Confirm key is still enabled (check mode didn't change it)" + ansible.builtin.assert: + that: + - user_after_check_disable.users.access_keys | selectattr('access_key', 'equalto', test_access_key) | map(attribute='enabled') | first == true + fail_msg: "Check mode should not have modified the key, but it did" + when: user_after_check_disable is not skipped + +# ==================== +# Update Second Key Tests (Verify Multiple Keys Work) +# ==================== + +- name: "Disable second access key (verify fix works with multiple keys)" + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ test_user_id }}" + access_key: "{{ test_access_key2 }}" + tenant_id: "{{ test_tenant_id }}" + enabled: false + state: present + register: disable_key2_result + when: can_run_tests | bool + +- name: "Verify second key disable succeeded" + ansible.builtin.assert: + that: + - disable_key2_result is changed + - disable_key2_result.result.enabled == false + - disable_key2_result.result.tenant_id == test_tenant_id + fail_msg: "Disabling second key failed" + when: disable_key2_result is not skipped + +- name: "Verify both keys exist with correct states" + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + state: present + register: user_both_keys + when: can_run_tests | bool + +- name: "Confirm both keys exist with expected states" + ansible.builtin.assert: + that: + - user_both_keys.users.access_keys | length == 2 + - user_both_keys.users.access_keys | selectattr('access_key', 'equalto', test_access_key) | map(attribute='enabled') | first == true + - user_both_keys.users.access_keys | selectattr('access_key', 'equalto', test_access_key2) | map(attribute='enabled') | first == false + fail_msg: "Both keys should exist with correct enabled states" + when: user_both_keys is not skipped + +# ==================== +# Identity Params Test (Verify tenant_id matching works) +# ==================== + +- name: "Test updating key without specifying tenant_id (should still work)" + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ test_user_id }}" + access_key: "{{ test_access_key2 }}" + enabled: true + state: present + register: update_without_tenant_result + when: can_run_tests | bool + +- name: "Verify update without tenant_id succeeded" + ansible.builtin.assert: + that: + - update_without_tenant_result is changed + - update_without_tenant_result.result.enabled == true + fail_msg: "Update should work even without specifying tenant_id" + when: update_without_tenant_result is not skipped + +# ==================== +# Check Mode Tests - Create +# ==================== + +- name: Test check mode - create access key (should report change) + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ test_user_id }}" + tenant_id: "{{ test_tenant_id }}" + state: present + check_mode: true + register: check_create_result + when: can_run_tests | bool + +- name: Verify check mode reported changes + ansible.builtin.assert: + that: + - check_create_result is changed + when: check_create_result is not skipped + +# ==================== +# Delete Access Key Tests +# ==================== + +- name: Test deleting first access key + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ test_user_id }}" + access_key: "{{ test_access_key }}" + state: absent + register: delete_result + when: can_run_tests | bool + +- name: Verify access key was deleted + ansible.builtin.assert: + that: + - delete_result is changed + - delete_result.result is defined + when: delete_result is not skipped + +# ==================== +# Check Mode Tests - Delete +# ==================== + +- name: Create access key for check mode delete test + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ test_user_id }}" + tenant_id: "{{ test_tenant_id }}" + state: present + register: check_delete_key + when: can_run_tests | bool + +- name: Test check mode - delete + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ test_user_id }}" + state: absent + check_mode: true + register: check_delete_result + when: can_run_tests | bool + +- name: Verify check mode reported deletion + ansible.builtin.assert: + that: + - check_delete_result is changed + when: check_delete_result is not skipped + +- name: Verify check mode didn't actually delete (query user) + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + state: present + register: user_after_check_delete + when: can_run_tests | bool + +- name: Check that access key still exists + ansible.builtin.assert: + that: + - user_after_check_delete.users.access_keys is defined + - user_after_check_delete.users.access_keys | length > 0 + when: user_after_check_delete is not skipped + +# ==================== +# Error Handling Tests +# ==================== + +- name: Test with non-existent user_id (should fail gracefully) + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: 999999 + tenant_id: "{{ test_tenant_id }}" + state: present + register: invalid_user_result + ignore_errors: true + when: can_run_tests | bool + +- name: Verify module fails with non-existent user + ansible.builtin.assert: + that: + - invalid_user_result is failed + fail_msg: "Module should fail when user_id doesn't exist" + when: invalid_user_result is not skipped + +# ============================================================ +# REGRESSION: cross-tenant key management +# ============================================================ + +- name: "REGRESSION setup: derive unique-per-run identifiers" + ansible.builtin.set_fact: + regression_run_id: "{{ ansible_date_time.epoch }}" + regression_tenant_name: "{{ test_regression_tenant_name }}-{{ ansible_date_time.epoch }}" + regression_user_name: "{{ test_regression_user_name }}-{{ ansible_date_time.epoch }}" + regression_user_uid: "{{ test_regression_user_uid + (ansible_date_time.epoch | int % 1000) }}" + when: can_run_tests | bool + +- name: "REGRESSION setup: create tenant" + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ regression_tenant_name }}" + state: present + register: regression_tenant + when: can_run_tests | bool + +- name: "REGRESSION setup: create user in the tenant's local provider" + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ regression_user_name }}" + uid: "{{ regression_user_uid }}" + local_provider_id: "{{ regression_tenant.tenants.local_provider_id }}" + state: present + register: regression_user + when: regression_tenant is not skipped + +- name: "REGRESSION setup: capture user/tenant ids" + ansible.builtin.set_fact: + regression_user_id: "{{ regression_user.users.id }}" + regression_tenant_id: "{{ regression_tenant.tenants.id }}" + when: regression_user is not skipped + +# ----- Step 1 (orion): create first access key ----- + +- name: "REGRESSION step 1: create first access key" + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ regression_user_id }}" + tenant_id: "{{ regression_tenant_id }}" + state: present + register: regression_key1 + when: can_run_tests | bool + +- name: "REGRESSION step 1: verify first key returned by module" + ansible.builtin.assert: + that: + - regression_key1 is changed + - regression_key1.result.access_key is defined + - regression_key1.result.secret_key is defined + fail_msg: "First key creation must return access_key and secret_key (orion step 1)" + when: regression_key1 is not skipped + +- name: "REGRESSION step 1: tenant-scoped query (after key #1)" + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + uid: "{{ regression_user_uid }}" + tenant_id: "{{ regression_tenant_id }}" + state: present + register: regression_query_after_create1 + when: can_run_tests | bool + +- name: "REGRESSION step 1: verify key #1 is visible via nonlocal_user (tenant scope)" + ansible.builtin.assert: + that: + - regression_query_after_create1.nonlocal_user.access_keys | length == 1 + - regression_key1.result.access_key in (regression_query_after_create1.nonlocal_user.access_keys | map(attribute='access_key') | list) + fail_msg: "Key #1 must be visible via nonlocal_user (users/query/) in the tenant scope" + when: regression_query_after_create1 is not skipped + +# ----- Step 2 (orion): create second access key ----- + +- name: "REGRESSION step 2: create second access key" + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ regression_user_id }}" + tenant_id: "{{ regression_tenant_id }}" + state: present + register: regression_key2 + when: can_run_tests | bool + +- name: "REGRESSION step 2: verify second key is new and distinct" + ansible.builtin.assert: + that: + - regression_key2 is changed + - regression_key2.result.access_key is defined + - regression_key2.result.access_key != regression_key1.result.access_key + fail_msg: "Second key must be created with a distinct access_key (orion step 2)" + when: regression_key2 is not skipped + +- name: "REGRESSION step 2: tenant-scoped query (after key #2)" + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + uid: "{{ regression_user_uid }}" + tenant_id: "{{ regression_tenant_id }}" + state: present + register: regression_query_after_create2 + when: can_run_tests | bool + +- name: "REGRESSION step 2: verify both keys are visible via nonlocal_user" + ansible.builtin.assert: + that: + - regression_query_after_create2.nonlocal_user.access_keys | length == 2 + - regression_key1.result.access_key in (regression_query_after_create2.nonlocal_user.access_keys | map(attribute='access_key') | list) + - regression_key2.result.access_key in (regression_query_after_create2.nonlocal_user.access_keys | map(attribute='access_key') | list) + fail_msg: "Both keys must be visible via nonlocal_user (users/query/) in the tenant scope" + when: regression_query_after_create2 is not skipped + +# ----- Extension (skipped in orion due to the bug — now covered): disable an existing key ----- +# This is the path that previously POSTed instead of PATCHed and hit +# "No more than 2 keys per tenant is allowed." + +- name: "REGRESSION ext: disable key #1 via access_key + state=present (was the bug)" + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ regression_user_id }}" + tenant_id: "{{ regression_tenant_id }}" + access_key: "{{ regression_key1.result.access_key }}" + enabled: false + state: present + register: regression_disable + when: can_run_tests | bool + +- name: "REGRESSION ext: verify disable PATCHed the existing key (no 3rd POST)" + ansible.builtin.assert: + that: + - regression_disable is changed + - regression_disable.result.enabled == false + - regression_disable.result.tenant_id | int == regression_tenant_id | int + when: regression_disable is not skipped + +- name: "REGRESSION ext: tenant-scoped query (after disable)" + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + uid: "{{ regression_user_uid }}" + tenant_id: "{{ regression_tenant_id }}" + state: present + register: regression_query_after_disable + when: can_run_tests | bool + +- name: "REGRESSION ext: confirm exactly 2 keys, key #1 disabled, key #2 still enabled" + ansible.builtin.assert: + that: + - regression_query_after_disable.nonlocal_user.access_keys | length == 2 + - regression_query_after_disable.nonlocal_user.access_keys | selectattr('access_key', 'equalto', regression_key1.result.access_key) | + map(attribute='enabled') | first == false + - regression_query_after_disable.nonlocal_user.access_keys | selectattr('access_key', 'equalto', regression_key2.result.access_key) | + map(attribute='enabled') | first == true + fail_msg: "After disable: expected 2 keys with key #1 disabled and key #2 enabled" + when: regression_query_after_disable is not skipped + +- name: "REGRESSION ext: repeat disable — must be idempotent (no change)" + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ regression_user_id }}" + tenant_id: "{{ regression_tenant_id }}" + access_key: "{{ regression_key1.result.access_key }}" + enabled: false + state: present + register: regression_disable_idempotent + when: can_run_tests | bool + +- name: "REGRESSION ext: verify repeated disable reported no change" + ansible.builtin.assert: + that: + - regression_disable_idempotent is not changed + fail_msg: "Cross-tenant disable must be idempotent — got changed=true on second call" + when: regression_disable_idempotent is not skipped + +# ----- Step 3 (orion): delete a specific key by access_key + state=absent ----- + +- name: "REGRESSION step 3: delete key #1 by access_key + state=absent" + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ regression_user_id }}" + access_key: "{{ regression_key1.result.access_key }}" + state: absent + when: can_run_tests | bool + +- name: "REGRESSION step 3: tenant-scoped query (after delete)" + vastdata.vms.nonlocal_user: + vms: "{{ vms_auth }}" + uid: "{{ regression_user_uid }}" + tenant_id: "{{ regression_tenant_id }}" + state: present + register: regression_query_after_delete + when: can_run_tests | bool + +- name: "REGRESSION step 3: confirm key #1 is deleted and key #2 remains" + ansible.builtin.assert: + that: + - regression_query_after_delete.nonlocal_user.access_keys | length == 1 + - regression_key1.result.access_key not in (regression_query_after_delete.nonlocal_user.access_keys | map(attribute='access_key') | list) + - regression_key2.result.access_key in (regression_query_after_delete.nonlocal_user.access_keys | map(attribute='access_key') | list) + fail_msg: "After delete: expected only key #2 to remain (orion step 3)" + when: regression_query_after_delete is not skipped + +# ----- Step 4 (orion): destroy — delete user (also removes remaining keys), delete tenant ----- + +- name: "REGRESSION destroy: delete regression user (cascade-removes remaining keys)" + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ regression_user_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: "REGRESSION destroy: delete the non-default tenant" + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "{{ regression_tenant_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: "REGRESSION destroy: delete the auto-created local provider" + vastdata.vms.localproviders: + vms: "{{ vms_auth }}" + name: "{{ regression_tenant_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up - delete all remaining access keys + vastdata.vms.user_key: + vms: "{{ vms_auth }}" + user_id: "{{ test_user_id }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove test user + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Skip Message +# ==================== + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping user_key integration tests - credentials not configured. + + To enable integration tests, set credentials in: + tests/integration/integration_config.yml + + Example: + vast_vms: "vast-vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration user_key -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/users/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/users/defaults/main.yml new file mode 100644 index 000000000..5a817de82 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/users/defaults/main.yml @@ -0,0 +1,22 @@ +--- +# Default variables for users integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +# Leave empty to skip tests gracefully +vast_vms: "" +vast_username: "" +vast_password: "" + +# Test user configuration +test_user_name: ansible-test-user +test_user_uid: 9999 +test_user_gid: 9999 + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/users/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/users/tasks/main.yml new file mode 100644 index 000000000..30816271d --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/users/tasks/main.yml @@ -0,0 +1,731 @@ +--- +# Integration tests for users module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials }}" + +- name: Get default tenant to obtain local_provider_id + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "default" + state: present + register: tenant_result + when: can_run_tests | bool + +- name: Set local_provider_id from default tenant + ansible.builtin.set_fact: + test_local_provider_id: "{{ tenant_result.tenants.local_provider_id }}" + when: can_run_tests | bool + +# ==================== +# Cleanup - Remove existing test user if present +# ==================== + +- name: Clean up - ensure test user doesn't exist before starting + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +# ==================== +# Create Tests +# ==================== + +- name: Test creating a user + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + uid: "{{ test_user_uid }}" + gids: ["{{ test_user_gid }}"] + leading_gid: "{{ test_user_gid }}" + local_provider_id: "{{ test_local_provider_id }}" + allow_create_bucket: false + allow_delete_bucket: false + s3_superuser: false + state: present + register: result + when: can_run_tests | bool + +- name: Verify user was created + ansible.builtin.assert: + that: + - result is changed + - result.users is defined + - result.users.name == test_user_name + - result.users.uid == test_user_uid + - result.diff is defined + - result.diff.before == {} + - result.diff.after is defined + when: result is not skipped + +# ==================== +# Idempotency Tests - Create +# ==================== + +- name: Test idempotency - create same user again + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + uid: "{{ test_user_uid }}" + gids: ["{{ test_user_gid }}"] + leading_gid: "{{ test_user_gid }}" + local_provider_id: "{{ test_local_provider_id }}" + allow_create_bucket: false + allow_delete_bucket: false + s3_superuser: false + state: present + register: result + when: can_run_tests | bool + +- name: Verify idempotency (resource unchanged after second run) + ansible.builtin.assert: + that: + - result.users is defined + - result.users.name == test_user_name + - result.users.uid == test_user_uid + when: result is not skipped + +# ==================== +# Update Tests +# ==================== + +- name: Test updating user - add GID + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + uid: "{{ test_user_uid }}" + gids: ["{{ test_user_gid }}", 10000] + leading_gid: "{{ test_user_gid }}" + local_provider_id: "{{ test_local_provider_id }}" + allow_create_bucket: false + allow_delete_bucket: false + s3_superuser: false + state: present + register: result + when: can_run_tests | bool + +- name: Verify user was updated + ansible.builtin.assert: + that: + - result is changed + - result.users is defined + - result.users.gids | length == 2 + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: result is not skipped + +- name: Test update idempotency + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + uid: "{{ test_user_uid }}" + gids: ["{{ test_user_gid }}", 10000] + leading_gid: "{{ test_user_gid }}" + local_provider_id: "{{ test_local_provider_id }}" + allow_create_bucket: false + allow_delete_bucket: false + s3_superuser: false + state: present + register: result + when: can_run_tests | bool + +- name: Verify update idempotency (resource unchanged after second update run) + ansible.builtin.assert: + that: + - result.users is defined + - result.users.name == test_user_name + - result.users.uid == test_user_uid + - result.users.gids | length == 2 + when: result is not skipped + +# ==================== +# Update S3 Permissions Tests +# ==================== + +- name: Test updating S3 permissions + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + allow_create_bucket: true + s3_superuser: true + state: present + register: result + when: can_run_tests | bool + +- name: Verify S3 permissions were updated + ansible.builtin.assert: + that: + - result is changed + - result.users is defined + - result.users.allow_create_bucket == true + - result.users.s3_superuser == true + when: result is not skipped + +# ==================== +# Check Mode Tests +# ==================== + +- name: Test check mode - make a change + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + allow_delete_bucket: true + state: present + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported changes + ansible.builtin.assert: + that: + - result is changed + - result.users is defined + when: result is not skipped + +- name: Verify check mode didn't actually make changes + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + state: present + register: result + when: can_run_tests | bool + +- name: Check that allow_delete_bucket is still false + ansible.builtin.assert: + that: + - result.users.allow_delete_bucket == false or result.users.allow_delete_bucket is not defined + when: result is not skipped + +# ==================== +# Special Operations Tests (via dedicated sub-endpoint modules) +# ==================== + +- name: Lookup user ID for sub-endpoint tests + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + state: present + register: user_lookup + when: can_run_tests | bool + +- name: Test tenant_data query via dedicated module + vastdata.vms.user_tenant_data: + vms: "{{ vms_auth }}" + user_id: "{{ user_lookup.users.id }}" + register: result + when: can_run_tests | bool + failed_when: false + +- name: Verify tenant_data query (may fail if not configured) + ansible.builtin.assert: + that: + - result is not changed or result is failed + when: result is not skipped + +# ==================== +# Delete Tests +# ==================== + +- name: Test deleting user + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify user was deleted + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after == {} + when: result is not skipped + +# ==================== +# Idempotency Tests - Delete +# ==================== + +- name: Test delete idempotency - delete again + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Check Mode Delete Test +# ==================== + +- name: Create user for check mode delete test + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-checkmode" + uid: 9998 + gids: [9998] + local_provider_id: "{{ test_local_provider_id }}" + state: present + register: result + when: can_run_tests | bool + +- name: Test check mode - delete + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-checkmode" + state: absent + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported deletion + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually delete + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-checkmode" + state: present + register: result + when: can_run_tests | bool + +- name: Check that user still exists + ansible.builtin.assert: + that: + - result is not changed + - result.users is defined + when: result is not skipped + +- name: Clean up check mode test user + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-checkmode" + state: absent + when: can_run_tests | bool + +# ==================== +# Password Field Tests +# ==================== + +- name: Clean up - ensure password test user doesn't exist before starting + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-password-test" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Test creating a user with password field + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-password-test" + uid: 19997 + gids: ["{{ test_user_gid }}"] + leading_gid: "{{ test_user_gid }}" + local_provider_id: "{{ test_local_provider_id }}" + password: "InitialTestPassword123!" + state: present + register: result + when: can_run_tests | bool + +- name: Verify user with password was created + ansible.builtin.assert: + that: + - result is changed + - result.users is defined + - result.users.name == test_user_name + '-password-test' + - result.diff is defined + - result.diff.before == {} + - result.diff.after is defined + when: result is not skipped + +- name: Test idempotency with password field (should not change) + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-password-test" + password: "InitialTestPassword123!" + state: present + register: result + when: can_run_tests | bool + +- name: Verify idempotency with same password (ephemeral fields excluded from updates) + ansible.builtin.assert: + that: + - result is not changed + - result.users is defined + fail_msg: | + Idempotency check failed. Ephemeral fields like 'password' are excluded from + update operations to maintain idempotency. To change a password, delete and + recreate the user. + when: result is not skipped + +- name: Test that password field is marked as no_log (verify no password in logs) + ansible.builtin.debug: + msg: "Password field should be masked in logs - verify no password appears in output" + when: can_run_tests | bool + +- name: Clean up password test user + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-password-test" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Idempotency local_provider_id test +# ==================== + +- name: Clean up - ensure local_provider_id idempotency test user doesn't exist + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-lp-idempotency" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Create local provider for local_provider_id idempotency test + vastdata.vms.localproviders: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-lp" + state: present + register: lp_idempotency + when: can_run_tests | bool + +- name: Set local_provider_id from created local provider + ansible.builtin.set_fact: + lp_idempotency_provider_id: "{{ lp_idempotency.localproviders.id }}" + when: can_run_tests | bool + +- name: Create user with local_provider_id (first run - should change) + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-lp-idempotency" + uid: 99901 + local_provider_id: "{{ lp_idempotency_provider_id }}" + state: present + register: lp_idempotency_result + when: can_run_tests | bool + +- name: Verify user was created (changed=1) + ansible.builtin.assert: + that: + - lp_idempotency_result is changed + - lp_idempotency_result.users is defined + - lp_idempotency_result.users.name == test_user_name + '-lp-idempotency' + when: lp_idempotency_result is not skipped + +- name: Run same create again - idempotency (second run - should NOT change) + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-lp-idempotency" + uid: 99901 + local_provider_id: "{{ lp_idempotency_provider_id }}" + state: present + register: lp_idempotency_result2 + when: can_run_tests | bool + +- name: Verify idempotency - second run must report changed=0 + ansible.builtin.assert: + that: + - not lp_idempotency_result2.changed + when: lp_idempotency_result2 is not skipped + +- name: Clean up local_provider_id idempotency test user + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-lp-idempotency" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up local provider from idempotency test + vastdata.vms.localproviders: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-lp" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Test: Users with same name in different local providers +# ==================== + +- name: Clean up - ensure duplicate user test resources don't exist + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-duplicate-user" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up - ensure test local provider 1 doesn't exist + vastdata.vms.localproviders: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-lp1" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up - ensure test local provider 2 doesn't exist + vastdata.vms.localproviders: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-lp2" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Create local provider 1 for duplicate user test + vastdata.vms.localproviders: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-lp1" + state: present + register: lp1 + when: can_run_tests | bool + +- name: Create local provider 2 for duplicate user test + vastdata.vms.localproviders: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-lp2" + state: present + register: lp2 + when: can_run_tests | bool + +- name: Create user with duplicate name in local provider 1 + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-duplicate-user" + uid: 99901 + local_provider_id: "{{ lp1.localproviders.id }}" + state: present + register: user1 + when: can_run_tests | bool + +- name: Verify first user was created + ansible.builtin.assert: + that: + - user1 is changed + - user1.users is defined + - user1.users.name == test_user_name + '-duplicate-user' + - user1.users.uid == 99901 + - user1.diff is defined + - user1.diff.before == {} + when: user1 is not skipped + +- name: Create user with SAME name in local provider 2 (should create new user, not update) + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-duplicate-user" + uid: 99902 + local_provider_id: "{{ lp2.localproviders.id }}" + state: present + register: user2 + when: can_run_tests | bool + +- name: Verify second user with same name was created (not updated) + ansible.builtin.assert: + that: + - user2 is changed + - user2.users is defined + - user2.users.name == test_user_name + '-duplicate-user' + - user2.users.uid == 99902 + - user2.diff is defined + - user2.diff.before == {} + - user2.diff.after is defined + fail_msg: | + Regression detected: Module failed to create user with same name in different local provider. + The module should support creating users with identical names across different local providers. + when: user2 is not skipped + +- name: Verify both users exist with different IDs + ansible.builtin.assert: + that: + - user1.users.id != user2.users.id + fail_msg: "Both users should have different IDs" + when: user1 is not skipped and user2 is not skipped + +- name: Test idempotency - recreate user1 in local provider 1 + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-duplicate-user" + uid: 99901 + local_provider_id: "{{ lp1.localproviders.id }}" + state: present + register: user1_idempotent + when: can_run_tests | bool + +- name: Verify idempotency for user in local provider 1 + ansible.builtin.assert: + that: + - not user1_idempotent.changed + - user1_idempotent.users.id == user1.users.id + when: user1_idempotent is not skipped and user1 is not skipped + +- name: Test idempotency - recreate user2 in local provider 2 + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-duplicate-user" + uid: 99902 + local_provider_id: "{{ lp2.localproviders.id }}" + state: present + register: user2_idempotent + when: can_run_tests | bool + +- name: Verify idempotency for user in local provider 2 + ansible.builtin.assert: + that: + - not user2_idempotent.changed + - user2_idempotent.users.id == user2.users.id + when: user2_idempotent is not skipped and user2 is not skipped + +- name: Clean up - delete user from local provider 1 + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-duplicate-user" + local_provider_id: "{{ lp1.localproviders.id }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up - delete user from local provider 2 + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-duplicate-user" + local_provider_id: "{{ lp2.localproviders.id }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up - delete local provider 1 + vastdata.vms.localproviders: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-lp1" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up - delete local provider 2 + vastdata.vms.localproviders: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-lp2" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# s3_policies_ids Test +# ==================== + +- name: Create S3 policy for testing s3_policies_ids + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-s3-policy" + policy: | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["s3:GetObject"], + "Resource": ["*"] + } + ] + } + tenant_id: 1 + state: present + register: s3_policy + when: can_run_tests | bool + +- name: Create user with s3_policies_ids + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-s3-user" + uid: 88888 + local_provider_id: 1 + s3_policies_ids: + - "{{ s3_policy.s3policies.id }}" + state: present + register: user_with_s3_policy + when: can_run_tests | bool + +- name: Verify user was created with s3_policies_ids + ansible.builtin.assert: + that: + - user_with_s3_policy is changed + - user_with_s3_policy.users is defined + - user_with_s3_policy.users.s3_policies_ids is defined + - s3_policy.s3policies.id in user_with_s3_policy.users.s3_policies_ids + fail_msg: "Failed to create user with s3_policies_ids" + when: user_with_s3_policy is not skipped and s3_policy is not skipped + +- name: Clean up - delete user with s3_policies_ids + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-s3-user" + local_provider_id: 1 + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up - delete S3 policy + vastdata.vms.s3policies: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}-s3-policy" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Cleanup +# ==================== + +- name: Final cleanup - remove test user if it exists + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "{{ test_user_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping users integration tests - credentials not configured. + + To enable integration tests, set credentials in: + tests/integration/integration_config.yml + + Example: + vast_vms: "vast-vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration users -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/viewpolicies/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/viewpolicies/defaults/main.yml new file mode 100644 index 000000000..42eb6fdd4 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/viewpolicies/defaults/main.yml @@ -0,0 +1,21 @@ +--- +# Default variables for viewpolicies integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +vast_vms: "" +vast_username: "" +vast_password: "" + +# View Policy test configuration +test_viewpolicy_name: "ansible-test-viewpolicy" +test_viewpolicy_flavor: "NFS" +test_viewpolicy_auth_source: "RPC" + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/viewpolicies/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/viewpolicies/tasks/main.yml new file mode 100644 index 000000000..6893654c4 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/viewpolicies/tasks/main.yml @@ -0,0 +1,406 @@ +--- +# Integration tests for viewpolicies module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials | bool }}" + +# ==================== +# Cleanup - Ensure clean state +# ==================== + +- name: Clean up - ensure test view policy doesn't exist before starting + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +# ==================== +# Create Tests +# ==================== + +- name: Test creating a view policy + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}" + flavor: "{{ test_viewpolicy_flavor }}" + auth_source: "{{ test_viewpolicy_auth_source }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify view policy was created + ansible.builtin.assert: + that: + - result is changed + - result.viewpolicies is defined + - result.viewpolicies.name == test_viewpolicy_name + - result.viewpolicies.flavor == test_viewpolicy_flavor + when: can_run_tests | bool + +# ==================== +# Idempotency Tests +# ==================== + +- name: Test idempotency - create same view policy again + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}" + flavor: "{{ test_viewpolicy_flavor }}" + auth_source: "{{ test_viewpolicy_auth_source }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify idempotency (should not change) + ansible.builtin.assert: + that: + - result is not changed + when: can_run_tests | bool + +# ==================== +# Update Tests +# ==================== + +- name: Test updating view policy - change auth_source + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}" + auth_source: "PROVIDERS" + state: present + register: result + when: can_run_tests | bool + +- name: Verify view policy was updated + ansible.builtin.assert: + that: + - result is changed + - result.viewpolicies.auth_source == "PROVIDERS" + when: can_run_tests | bool + +- name: Test update idempotency + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}" + auth_source: "PROVIDERS" + state: present + register: result + when: can_run_tests | bool + +- name: Verify update idempotency + ansible.builtin.assert: + that: + - result is not changed + when: can_run_tests | bool + +# ==================== +# Update NFS Settings +# ==================== + +- name: Test updating NFS read/write settings + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}" + nfs_read_write: + - "*" + nfs_no_squash: + - "*" + state: present + register: result + when: can_run_tests | bool + +- name: Verify NFS settings were updated + ansible.builtin.assert: + that: + - result is changed + when: can_run_tests | bool + +# ==================== +# Update GID Inheritance +# ==================== + +- name: Test updating gid_inheritance + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}" + gid_inheritance: "BSD" + state: present + register: result + when: can_run_tests | bool + +- name: Verify gid_inheritance was updated + ansible.builtin.assert: + that: + - result is changed + - result.viewpolicies.gid_inheritance == "BSD" + when: can_run_tests | bool + +# ==================== +# Check Mode Tests +# ==================== + +- name: Set auth_source back for check mode baseline + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}" + auth_source: "PROVIDERS" + gid_inheritance: "BSD" + state: present + register: baseline_result + when: can_run_tests | bool + +- name: Test check mode - update auth_source + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}" + auth_source: "RPC_AND_PROVIDERS" + state: present + register: result + check_mode: true + when: can_run_tests | bool + +- name: Verify check mode reported change + ansible.builtin.assert: + that: + - result is changed + when: can_run_tests | bool + +- name: Verify check mode didn't actually change + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}" + auth_source: "PROVIDERS" + state: present + register: result + when: can_run_tests | bool + +- name: Check that auth_source was not changed + ansible.builtin.assert: + that: + - result is not changed + when: can_run_tests | bool + +# ==================== +# Diff Mode Tests +# ==================== + +- name: Test diff mode - update gid_inheritance + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}" + gid_inheritance: "LINUX" + state: present + register: result + diff: true + when: can_run_tests | bool + +- name: Verify diff mode returned diff data + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: can_run_tests | bool + +# ==================== +# Delete Tests +# ==================== + +- name: Test deleting view policy + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify view policy was deleted + ansible.builtin.assert: + that: + - result is changed + when: can_run_tests | bool + +# ==================== +# Delete Idempotency Test +# ==================== + +- name: Test delete idempotency - delete again + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed + when: can_run_tests | bool + +# ==================== +# Check Mode Delete Tests +# ==================== + +- name: Create view policy for check mode delete test + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}-delcheck" + flavor: "{{ test_viewpolicy_flavor }}" + state: present + register: result + when: can_run_tests | bool + +- name: Test check mode - delete + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}-delcheck" + state: absent + register: result + check_mode: true + when: can_run_tests | bool + +- name: Verify check mode reported deletion + ansible.builtin.assert: + that: + - result is changed + when: can_run_tests | bool + +- name: Verify check mode didn't actually delete + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}-delcheck" + state: present + register: result + when: can_run_tests | bool + +- name: Check that view policy still exists + ansible.builtin.assert: + that: + - result is not changed + when: can_run_tests | bool + +- name: Clean up check mode delete test view policy + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}-delcheck" + state: absent + register: result + when: can_run_tests | bool + failed_when: false + +# ==================== +# create viewpolicy with tenant_id +# ==================== + +- name: Create tenant + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "test-tenant" + client_ip_ranges: [["192.168.1.1", "192.168.1.2"]] + state: present + register: tenant_result + +- name: Test creating a view policy with tenant_id + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}-tenant" + tenant_id: "{{ tenant_result.tenants.id }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify view policy was created with tenant_id + ansible.builtin.assert: + that: + - result is changed + - result.viewpolicies.tenant_id == tenant_result.tenants.id + when: can_run_tests | bool + +- name: Clean up test view policy with tenant_id + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}-tenant" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Error Handling Tests +# ==================== + +- name: Test error handling - invalid state + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}" + state: invalid + register: result + ignore_errors: true + when: can_run_tests | bool + +- name: Verify error was caught + ansible.builtin.assert: + that: + - result is failed + - "'invalid' in result.msg" + when: can_run_tests | bool + +# ==================== +# Final Cleanup +# ==================== + +- name: Final cleanup - remove test view policy if it exists + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove test view policy if it exists + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}-tenant" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove delcheck test view policy if it exists + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "{{ test_viewpolicy_name }}-delcheck" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Skip Message +# ==================== + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping viewpolicies integration tests - missing credentials. + + To enable integration tests, set the following in: + tests/integration/integration_config.yml + + VAST credentials: + vast_vms: "vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration viewpolicies -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/views/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/views/defaults/main.yml new file mode 100644 index 000000000..7ffc6b677 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/views/defaults/main.yml @@ -0,0 +1,24 @@ +--- +# Default variables for views integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +# Leave empty to skip tests gracefully +vast_vms: "" +vast_username: "" +vast_password: "" + +# Test view configuration +test_view_name: ansible-test-view +test_view_path: /ansible-test-view +test_view_policy_id: 1 +test_view_protocols: + - NFS + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/views/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/views/tasks/main.yml new file mode 100644 index 000000000..72d1c5a6f --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/views/tasks/main.yml @@ -0,0 +1,814 @@ +--- +# Integration tests for views module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials }}" + +- name: Get default tenant ID for testing + ansible.builtin.uri: + url: "https://{{ vast_vms }}/api/tenants/?page_size=1" + method: GET + user: "{{ vast_username }}" + password: "{{ vast_password }}" + force_basic_auth: true + validate_certs: false + return_content: true + register: tenants_result + when: can_run_tests | bool + failed_when: false + +- name: Set default tenant ID from query result + ansible.builtin.set_fact: + test_view_tenant_id: "{{ tenants_result.json[0].id | default(1) }}" + when: can_run_tests | bool and tenants_result.json is defined and tenants_result.json | length > 0 + +# ==================== +# Cleanup - Remove existing test view if present +# ==================== + +- name: Clean up - ensure test view doesn't exist before starting + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + force: true + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +- name: Clean up - ensure tenant test view doesn't exist before starting + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}-tenant" + force: true + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Create Tests +# ==================== + +- name: Test creating a view + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + policy_id: "{{ test_view_policy_id }}" + protocols: "{{ test_view_protocols }}" + create_dir: true + state: present + register: result + when: can_run_tests | bool + +- name: Verify view was created + ansible.builtin.assert: + that: + - result is changed + - result.views is defined + - result.views.path == test_view_path + - result.diff is defined + - result.diff.before == {} + - result.diff.after is defined + when: result is not skipped + +# ==================== +# Idempotency Tests - Create +# ==================== + +- name: Test idempotency - create same view again + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + policy_id: "{{ test_view_policy_id }}" + protocols: "{{ test_view_protocols }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify idempotency (should not change) + ansible.builtin.assert: + that: + - result is not changed + - result.views is defined + when: result is not skipped + +# ==================== +# Update Tests +# ==================== + +- name: Test updating view - set alias + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + alias: "/ansible-test-alias-updated" + state: present + register: result + when: can_run_tests | bool + +- name: Verify view was updated + ansible.builtin.assert: + that: + - result is changed + - result.views is defined + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: result is not skipped + +- name: Test update idempotency + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + alias: "/ansible-test-alias-updated" + state: present + register: result + when: can_run_tests | bool + +- name: Verify update idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Check Mode Tests - Update +# ==================== + +- name: Test check mode - update alias + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + alias: "/ansible-check-alias" + state: present + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported changes + ansible.builtin.assert: + that: + - result is changed + - result.views is defined + when: result is not skipped + +- name: Verify check mode didn't actually make changes + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + alias: "/ansible-test-alias-updated" + state: present + register: result + when: can_run_tests | bool + +- name: Verify alias unchanged by check mode + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Check Mode Tests - Create +# ==================== + +- name: Test check mode - create new view + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}-checkmode" + policy_id: "{{ test_view_policy_id }}" + state: present + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode create reported changes + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually create the view + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}-checkmode" + force: true + state: absent + register: result + when: can_run_tests | bool + +- name: Verify view was never created + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Diff Mode Tests +# ==================== + +- name: Update view with diff mode + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + alias: "/ansible-diff-alias" + state: present + diff: true + register: result + when: can_run_tests | bool + +- name: Verify update diff output + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: result is not skipped + +# ==================== +# Delete Tests +# ==================== + +- name: Test deleting view + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + force: true + state: absent + register: result + when: can_run_tests | bool + +- name: Verify view was deleted + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after == {} + when: result is not skipped + +# ==================== +# Idempotency Tests - Delete +# ==================== + +- name: Test delete idempotency - delete again + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + force: true + state: absent + register: result + when: can_run_tests | bool + +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Check Mode Delete Test +# ==================== + +- name: Create view for check mode delete test + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}-delcheck" + policy_id: "{{ test_view_policy_id }}" + create_dir: true + state: present + register: result + when: can_run_tests | bool + +- name: Test check mode - delete + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}-delcheck" + force: true + state: absent + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported deletion + ansible.builtin.assert: + that: + - result is changed + when: result is not skipped + +- name: Verify check mode didn't actually delete + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}-delcheck" + state: present + register: result + when: can_run_tests | bool + +- name: Check that view still exists + ansible.builtin.assert: + that: + - result is not changed + - result.views is defined + when: result is not skipped + +- name: Clean up check mode delete test view + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}-delcheck" + force: true + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Error Handling Tests +# ==================== + +- name: Test error handling - invalid state + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + state: invalid + register: result + when: can_run_tests | bool + ignore_errors: true + +- name: Verify error was caught + ansible.builtin.assert: + that: + - result is failed + when: result is not skipped + +# ==================== +# Tenant ID Tests +# ==================== + +- name: Test creating a view with tenant_id + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}-tenant" + policy_id: "{{ test_view_policy_id }}" + tenant_id: "{{ test_view_tenant_id }}" + protocols: "{{ test_view_protocols }}" + create_dir: true + state: present + register: result + when: can_run_tests | bool and test_view_tenant_id is defined + +- name: Verify view was created with tenant_id + ansible.builtin.assert: + that: + - result is changed + - result.views is defined + - result.views.path == test_view_path + '-tenant' + - result.views.tenant_id == test_view_tenant_id + - result.diff is defined + when: result is not skipped and test_view_tenant_id is defined + +- name: Clean up tenant_id test view + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}-tenant" + force: true + state: absent + when: can_run_tests | bool and test_view_tenant_id is defined + failed_when: false + +# ==================== +# Idempotency Tests - create_dir +# ==================== + +- name: Clean up - ensure create_dir idempotency test view doesn't exist + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}-create-dir-idempotency" + force: true + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Create view with create_dir for idempotency test + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}-create-dir-idempotency" + policy_id: "{{ test_view_policy_id }}" + protocols: "{{ test_view_protocols }}" + create_dir: true + state: present + register: create_dir_idempotency_result + when: can_run_tests | bool + +- name: Verify view was created with create_dir + ansible.builtin.assert: + that: + - create_dir_idempotency_result is changed + - create_dir_idempotency_result.views is defined + - create_dir_idempotency_result.views.path == test_view_path + '-create-dir-idempotency' + when: create_dir_idempotency_result is not skipped + +- name: Test idempotency - run same task with create_dir on existing view + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}-create-dir-idempotency" + policy_id: "{{ test_view_policy_id }}" + protocols: "{{ test_view_protocols }}" + create_dir: true + state: present + register: create_dir_idempotency_result + when: can_run_tests | bool + +- name: Verify idempotency with create_dir (ephemeral fields excluded from updates) + ansible.builtin.assert: + that: + - create_dir_idempotency_result is not changed + - create_dir_idempotency_result.views is defined + when: create_dir_idempotency_result is not skipped + +- name: Clean up create_dir idempotency test view + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}-create-dir-idempotency" + force: true + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Idempotency Tests - bucket_logging +# ==================== + +- name: Clean up - ensure bucket logging test source view doesn't exist + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "/ans-efficient-pitohui2" + force: true + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up - ensure bucket logging test destination view doesn't exist + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "/ans-efficient-pitohui1" + force: true + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up - ensure bucket logging test tenant doesn't exist + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "ans-agreeable-thrasher" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Create tenant for bucket logging test + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "ans-agreeable-thrasher" + client_ip_ranges: + - ["192.168.200.1", "192.168.200.10"] + state: present + register: tenant + when: can_run_tests | bool + +- name: Create user for bucket owner + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "ans-loving-xenops" + uid: 12345 + local_provider_id: "{{ tenant.tenants.local_provider_id }}" + state: present + when: can_run_tests | bool + +- name: Create view policy for bucket logging test + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "ans-kind-lamprey" + flavor: "S3_NATIVE" + tenant_id: "{{ tenant.tenants.id }}" + state: present + register: policy + when: can_run_tests | bool + +- name: Create destination bucket view + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "/ans-efficient-pitohui1" + bucket: "ans-efficient-pitohui1" + bucket_owner: "ans-loving-xenops" + policy_id: "{{ policy.viewpolicies.id }}" + tenant_id: "{{ tenant.tenants.id }}" + protocols: ["S3"] + create_dir: true + state: present + register: view_dest + when: can_run_tests | bool + +- name: Create source bucket view with logging + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "/ans-efficient-pitohui2" + bucket: "ans-efficient-pitohui2" + bucket_owner: "ans-loving-xenops" + policy_id: "{{ policy.viewpolicies.id }}" + tenant_id: "{{ tenant.tenants.id }}" + protocols: ["S3"] + create_dir: true + bucket_logging: + destination_id: "{{ view_dest.views.id }}" + prefix: "pref" + key_format: "SIMPLE_PREFIX" + state: present + register: bucket_logging_result + when: can_run_tests | bool + +- name: Verify view was created with bucket_logging + ansible.builtin.assert: + that: + - bucket_logging_result is changed + - bucket_logging_result.views is defined + when: bucket_logging_result is not skipped + +- name: Test idempotency - run same task with bucket_logging again + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "/ans-efficient-pitohui2" + bucket: "ans-efficient-pitohui2" + bucket_owner: "ans-loving-xenops" + policy_id: "{{ policy.viewpolicies.id }}" + tenant_id: "{{ tenant.tenants.id }}" + protocols: ["S3"] + bucket_logging: + destination_id: "{{ view_dest.views.id }}" + prefix: "pref" + key_format: "SIMPLE_PREFIX" + state: present + register: bucket_logging_idempotency_result + when: can_run_tests | bool + +- name: Verify idempotency with bucket_logging (should not report changed) + ansible.builtin.assert: + that: + - bucket_logging_idempotency_result is not changed + - bucket_logging_idempotency_result.views is defined + fail_msg: "bucket_logging breaks idempotency - view reported as changed when it shouldn't be" + when: bucket_logging_idempotency_result is not skipped + +- name: Clean up bucket logging test - remove source view + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "/ans-efficient-pitohui2" + force: true + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up bucket logging test - remove destination view + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "/ans-efficient-pitohui1" + force: true + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up bucket logging test - remove policy + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "ans-kind-lamprey" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up bucket logging test - remove user + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "ans-loving-xenops" + local_provider_id: "{{ tenant.tenants.local_provider_id }}" + state: absent + when: can_run_tests | bool and tenant is defined and tenant.tenants is defined + failed_when: false + +- name: Clean up bucket logging test - remove tenant + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "ans-agreeable-thrasher" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Idempotency Tests - share_acl with fqdn normalization +# ==================== + +- name: Clean up - ensure share_acl test tenant doesn't exist + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "ans-inspiring-porpoise" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Create tenant for share_acl test + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "ans-inspiring-porpoise" + client_ip_ranges: [["192.168.205.100", "192.168.205.110"]] + state: present + register: share_acl_tenant + when: can_run_tests | bool + +- name: Create user 1 for share_acl test + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "ans-sturdy-pademelon" + uid: 747680 + local_provider_id: "{{ share_acl_tenant.tenants.local_provider_id }}" + state: present + when: can_run_tests | bool + +- name: Create user 2 for share_acl test + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "ans-frank-vaquita" + uid: 677991 + local_provider_id: "{{ share_acl_tenant.tenants.local_provider_id }}" + state: present + when: can_run_tests | bool + +- name: Create view policy for share_acl test + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "ans-lovely-binturong" + flavor: "S3_NATIVE" + tenant_id: "{{ share_acl_tenant.tenants.id }}" + state: present + register: share_acl_policy + when: can_run_tests | bool + +- name: Create S3 view with share_acl + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "/ans-laughing-dikkop" + bucket: "ans-laughing-dikkop" + bucket_owner: "ans-sturdy-pademelon" + policy_id: "{{ share_acl_policy.viewpolicies.id }}" + tenant_id: "{{ share_acl_tenant.tenants.id }}" + protocols: ["S3"] + create_dir: true + share_acl: + enabled: true + acl: + - name: "ans-sturdy-pademelon" + grantee: users + perm: "FULL" + fqdn: All + - name: "ans-frank-vaquita" + grantee: users + perm: "READ" + fqdn: All + state: present + register: share_acl_result + when: can_run_tests | bool + +- name: Verify view was created with share_acl + ansible.builtin.assert: + that: + - share_acl_result is changed + - share_acl_result.views is defined + when: share_acl_result is not skipped + +- name: Test idempotency - run same task with share_acl again + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "/ans-laughing-dikkop" + bucket: "ans-laughing-dikkop" + bucket_owner: "ans-sturdy-pademelon" + policy_id: "{{ share_acl_policy.viewpolicies.id }}" + tenant_id: "{{ share_acl_tenant.tenants.id }}" + protocols: ["S3"] + share_acl: + enabled: true + acl: + - name: "ans-sturdy-pademelon" + grantee: users + perm: "FULL" + fqdn: All + - name: "ans-frank-vaquita" + grantee: users + perm: "READ" + fqdn: All + state: present + register: share_acl_idempotency_result + when: can_run_tests | bool + +- name: Verify idempotency with share_acl (should not report changed) + ansible.builtin.assert: + that: + - share_acl_idempotency_result is not changed + fail_msg: "share_acl breaks idempotency - view reported as changed when it shouldn't be" + when: share_acl_idempotency_result is not skipped + +- name: Clean up share_acl test - remove view + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "/ans-laughing-dikkop" + force: true + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up share_acl test - remove policy + vastdata.vms.viewpolicies: + vms: "{{ vms_auth }}" + name: "ans-lovely-binturong" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Clean up share_acl test - remove user 1 + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "ans-sturdy-pademelon" + local_provider_id: "{{ share_acl_tenant.tenants.local_provider_id }}" + state: absent + when: can_run_tests | bool and share_acl_tenant is defined and share_acl_tenant.tenants is defined + failed_when: false + +- name: Clean up share_acl test - remove user 2 + vastdata.vms.users: + vms: "{{ vms_auth }}" + name: "ans-frank-vaquita" + local_provider_id: "{{ share_acl_tenant.tenants.local_provider_id }}" + state: absent + when: can_run_tests | bool and share_acl_tenant is defined and share_acl_tenant.tenants is defined + failed_when: false + +- name: Clean up share_acl test - remove tenant + vastdata.vms.tenants: + vms: "{{ vms_auth }}" + name: "ans-inspiring-porpoise" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Cleanup +# ==================== + +- name: Final cleanup - remove test view if it exists + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}" + force: true + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove delcheck test view if it exists + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}-delcheck" + force: true + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove tenant test view if it exists + vastdata.vms.views: + vms: "{{ vms_auth }}" + path: "{{ test_view_path }}-tenant" + force: true + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping views integration tests - credentials not configured. + + To enable integration tests, set credentials in: + tests/integration/integration_config.yml + + Example: + vast_vms: "vast-vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration views -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/vippools/defaults/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/vippools/defaults/main.yml new file mode 100644 index 000000000..0a2244190 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/vippools/defaults/main.yml @@ -0,0 +1,37 @@ +--- +# Default variables for vippools integration tests +# Override these in tests/integration/integration_config.yml + +# VAST system connection details +# Leave empty to skip tests gracefully +vast_vms: "" +vast_username: "" +vast_password: "" + +# Test VIP pool configuration - IPv6 +test_vippool_name: ansible-test-vippool-ipv6 +test_vippool_renamed: ansible-test-vippool-renamed-ipv6 +test_vippool_subnet_cidr_ipv6: 64 +test_vippool_ip_ranges: + - ["fec0:10::a", "fec0:10::14"] + +# Test VIP pool configuration - IPv4 +test_vippool_name_ipv4: ansible-test-vippool-ipv4 +test_vippool_renamed_ipv4: ansible-test-vippool-renamed-ipv4 +test_vippool_subnet_cidr: 24 +test_vippool_ip_ranges_ipv4: + - ["192.168.100.10", "192.168.100.20"] + +# Test VIP pool configuration - Dual Stack (IPv4 + IPv6) +test_vippool_name_dual: ansible-test-vippool-dual +test_vippool_renamed_dual: ansible-test-vippool-renamed-dual + +test_role: "PROTOCOLS" + +# Reusable connection dict — reference as ``vms: "{{ vms_auth }}"`` in tasks +vms_auth: + host: "{{ vast_vms }}" + username: "{{ vast_username }}" + password: "{{ vast_password }}" + validate_certs: false + debug: true diff --git a/vendor/ansible_collections/vastdata/vms/tests/integration/targets/vippools/tasks/main.yml b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/vippools/tasks/main.yml new file mode 100644 index 000000000..73f251c36 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/integration/targets/vippools/tasks/main.yml @@ -0,0 +1,414 @@ +--- +# Integration tests for vippools module +# These tests run against a real VAST system +# +# Configuration: +# Set values in tests/integration/integration_config.yml + +# ==================== +# Setup +# ==================== + +- name: Check if credentials are available + ansible.builtin.set_fact: + has_credentials: "{{ (vast_vms | default('') | length > 0) and (vast_username | default('') | length > 0) and (vast_password | default('') | length > 0) }}" + +- name: Set test conditions + ansible.builtin.set_fact: + can_run_tests: "{{ has_credentials }}" + +# ==================== +# Cleanup - Remove existing test VIP pools if present +# ==================== + +- name: Clean up - ensure test VIP pool doesn't exist before starting + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_name }}" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +- name: Clean up - ensure renamed test VIP pool doesn't exist + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_renamed }}" + state: absent + register: cleanup_result + when: can_run_tests | bool + failed_when: false + +# ==================== +# Create Tests +# ==================== + +- name: Test creating a VIP pool + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_name }}" + subnet_cidr_ipv6: "{{ test_vippool_subnet_cidr_ipv6 }}" + ip_ranges: "{{ test_vippool_ip_ranges }}" + enabled: false + state: present + register: result + when: can_run_tests | bool + +- name: Verify VIP pool was created + ansible.builtin.assert: + that: + - result is changed + - result.vippools is defined + - result.vippools.name == test_vippool_name + - result.vippools.id is defined + - result.diff is defined + - result.diff.before == {} + - result.diff.after is defined + when: result is not skipped + +- name: Save VIP pool ID for later tests + ansible.builtin.set_fact: + test_vippool_id: "{{ result.vippools.id }}" + when: result is not skipped + +# ==================== +# Idempotency Tests - Create +# ==================== + +- name: Test idempotency - create same VIP pool again + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_name }}" + subnet_cidr_ipv6: "{{ test_vippool_subnet_cidr_ipv6 }}" + ip_ranges: "{{ test_vippool_ip_ranges }}" + enabled: false + state: present + register: result + when: can_run_tests | bool + +- name: Verify idempotency (should not change) + ansible.builtin.assert: + that: + - result is not changed + - result.vippools is defined + - result.vippools.id == test_vippool_id + when: result is not skipped + +# ==================== +# Update Tests +# ==================== + +- name: Test updating VIP pool - enable + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_name }}" + enabled: true + state: present + register: result + when: can_run_tests | bool + +- name: Verify VIP pool was updated + ansible.builtin.assert: + that: + - result is changed + - result.vippools is defined + - result.vippools.id == test_vippool_id + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: result is not skipped + +- name: Test update idempotency + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_name }}" + enabled: true + state: present + register: result + when: can_run_tests | bool + +- name: Verify update idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Rename Tests +# ==================== + +- name: Test renaming VIP pool using ID parameter + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + id: "{{ test_vippool_id }}" + name: "{{ test_vippool_renamed }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify VIP pool was renamed + ansible.builtin.assert: + that: + - result is changed + - result.vippools is defined + - result.vippools.name == test_vippool_renamed + - result.vippools.id == test_vippool_id # ID should remain the same + - result.diff is defined + - result.diff.before.name == test_vippool_name + - result.diff.after.name == test_vippool_renamed + when: result is not skipped + +- name: Test rename idempotency + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + id: "{{ test_vippool_id }}" + name: "{{ test_vippool_renamed }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify rename idempotency + ansible.builtin.assert: + that: + - result is not changed + - result.vippools.name == test_vippool_renamed + when: result is not skipped + +- name: Test lookup by new name after rename + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_renamed }}" + state: present + register: result + when: can_run_tests | bool + +- name: Verify lookup by new name works + ansible.builtin.assert: + that: + - result is not changed + - result.vippools is defined + - result.vippools.id == test_vippool_id + - result.vippools.name == test_vippool_renamed + when: result is not skipped + +# ==================== +# Check Mode Tests - Update +# ==================== + +- name: Test check mode - update enabled + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_renamed }}" + enabled: false + state: present + check_mode: true + register: result + when: can_run_tests | bool + +- name: Verify check mode reported changes + ansible.builtin.assert: + that: + - result is changed + - result.vippools is defined + when: result is not skipped + +- name: Verify check mode didn't actually make changes + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_renamed }}" + enabled: true + state: present + register: result + when: can_run_tests | bool + +- name: Verify enabled unchanged by check mode + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Diff Mode Tests +# ==================== + +- name: Update VIP pool with diff mode + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_renamed }}" + enabled: false + state: present + diff: true + register: result + when: can_run_tests | bool + +- name: Verify update diff output + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after is defined + when: result is not skipped + +# ==================== +# Delete Tests +# ==================== + +- name: Test deleting VIP pool + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_renamed }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify VIP pool was deleted + ansible.builtin.assert: + that: + - result is changed + - result.diff is defined + - result.diff.before is defined + - result.diff.after == {} + when: result is not skipped + +# ==================== +# Idempotency Tests - Delete +# ==================== + +- name: Test delete idempotency - delete again + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_renamed }}" + state: absent + register: result + when: can_run_tests | bool + +- name: Verify delete idempotency + ansible.builtin.assert: + that: + - result is not changed + when: result is not skipped + +# ==================== +# Delete by ID Test +# ==================== + +- name: Create VIP pool for delete by ID test + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_name }}-delid" + subnet_cidr_ipv6: "{{ test_vippool_subnet_cidr_ipv6 }}" + ip_ranges: [["fec0:10::20", "fec0:10::24"]] + state: present + register: result + when: can_run_tests | bool + +- name: Test delete by ID + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + id: "{{ result.vippools.id }}" + state: absent + register: delete_result + when: result is not skipped + +- name: Verify VIP pool was deleted by ID + ansible.builtin.assert: + that: + - delete_result is changed + when: delete_result is not skipped + +# ==================== +# Create vippool with role +# ==================== + +- name: Create vippool with role + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_name }}-role" + role: "{{ test_role }}" + subnet_cidr: 24 + ip_ranges: [["192.168.99.1", "192.168.99.5"]] + state: present + register: result + when: can_run_tests | bool + +- name: Verify vippool was created with role + ansible.builtin.assert: + that: + - result is changed + - result.vippools is defined + - result.vippools.role == test_role + when: result is not skipped + +- name: Clean up test vippool with role + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_name }}-role" + state: absent + when: can_run_tests | bool + failed_when: false + +# ==================== +# Error Handling Tests +# ==================== + +- name: Test error handling - neither name nor ID provided + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + subnet_cidr_ipv6: "{{ test_vippool_subnet_cidr_ipv6 }}" + ip_ranges: "{{ test_vippool_ip_ranges }}" + state: present + register: result + when: can_run_tests | bool + ignore_errors: true + +- name: Verify error was caught + ansible.builtin.assert: + that: + - result is failed + - "'name' in result.msg or 'id' in result.msg" + when: result is not skipped + +# ==================== +# Cleanup +# ==================== + +- name: Final cleanup - remove test VIP pool if it exists + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_name }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove renamed test VIP pool if it exists + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_renamed }}" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Final cleanup - remove delid test VIP pool if it exists + vastdata.vms.vippools: + vms: "{{ vms_auth }}" + name: "{{ test_vippool_name }}-delid" + state: absent + when: can_run_tests | bool + failed_when: false + +- name: Skip message when no credentials + ansible.builtin.debug: + msg: | + Skipping vippools integration tests - credentials not configured. + + To enable integration tests, set credentials in: + tests/integration/integration_config.yml + + Example: + vast_vms: "vast-vms.example.com" + vast_username: "admin" + vast_password: "password" + + Then run: ansible-test integration vippools -v + when: not (can_run_tests | bool) diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/.gitkeep b/vendor/ansible_collections/vastdata/vms/tests/unit/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/__init__.py b/vendor/ansible_collections/vastdata/vms/tests/unit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/__init__.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/module_utils/__init__.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/module_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/module_utils/test_delete_params.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/module_utils/test_delete_params.py new file mode 100644 index 000000000..3d8de0dff --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/module_utils/test_delete_params.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for DELETE parameter routing through client and BaseResource.""" + +import sys +from pathlib import Path +from unittest.mock import MagicMock + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.client import ( + VastClient, + VastConnection, + _APIPath, +) +from plugins.module_utils.vast.resource import BaseResource + + +class _FakeClient: + """Stand-in for VastClient that records ``_request`` calls.""" + + def __init__(self): + self.calls = [] + + def _request(self, method, segments, *, params=None, data=None): + self.calls.append({"method": method, "segments": segments, "params": params, "data": data}) + return {"ok": True} + + +def _make_resource(delete_query=None, delete_body=None, params=None): + """Build a BaseResource instance bypassing __init__ (no live VMS).""" + + class _R(BaseResource): + resource_name = "things" + singular = "thing" + delete_query_params = set(delete_query or []) + delete_body_params = set(delete_body or []) + + res = _R.__new__(_R) + res.module = MagicMock() + res.params = params or {} + res.check_mode = False + res.client = MagicMock() + res.overrides = {} + return res + + +def _make_client(): + """Build a VastClient with a stubbed requests.Session.""" + client = VastClient.__new__(VastClient) + client._debug_traces = [] + client.debug = False + client._connection = VastConnection(host="example.com") + client._base_url = "https://example.com/api" + client._version = "latest" + client._timeout = None + + resp = MagicMock() + resp.status_code = 204 + resp.content = b"" + resp.headers = {} + resp.text = "" + resp.raise_for_status = MagicMock() + + session = MagicMock() + session.request = MagicMock(return_value=resp) + client._session = session + return client, session + + +class TestAPIPathDelete: + """Test _APIPath.delete keyword routing.""" + + def test_routes_query_and_body(self): + """Verify _query_params goes to params and other kwargs go to data.""" + client = _FakeClient() + _APIPath(client, ("views", "42")).delete(_query_params={"force": True}, remove_dir=True) + call = client.calls[0] + assert call["method"] == "DELETE" + assert call["segments"] == ("views", "42") + assert call["params"] == {"force": True} + assert call["data"] == {"remove_dir": True} + + def test_no_args_sends_none(self): + """Verify delete with no args sends None for both params and data.""" + client = _FakeClient() + _APIPath(client, ("v",)).delete() + assert client.calls[0]["params"] is None + assert client.calls[0]["data"] is None + + def test_only_body(self): + """Verify body-only delete leaves params empty.""" + client = _FakeClient() + _APIPath(client, ("v",)).delete(reason="x") + assert client.calls[0]["params"] is None + assert client.calls[0]["data"] == {"reason": "x"} + + def test_only_query(self): + """Verify query-only delete leaves data empty.""" + client = _FakeClient() + _APIPath(client, ("v",)).delete(_query_params={"force": True}) + assert client.calls[0]["params"] == {"force": True} + assert client.calls[0]["data"] is None + + +class TestRequestQueryOnNonGet: + """Test VastClient._request query-string branch on non-GET verbs.""" + + def test_delete_sends_query_and_body(self): + """Verify DELETE sends both query params and JSON body.""" + client, session = _make_client() + client._request( + "DELETE", + ("views", "42"), + params={"force": True, "tags": ["a", "b"]}, + data={"reason": "x"}, + ) + args, kwargs = session.request.call_args + assert args == ("DELETE", "https://example.com/api/latest/views/42/") + assert kwargs["params"] == [("force", True), ("tags", "a"), ("tags", "b")] + assert kwargs["data"] == '{"reason": "x"}' + + def test_delete_with_only_query(self): + """Verify DELETE without body omits the data kwarg.""" + client, session = _make_client() + client._request("DELETE", ("v", "1"), params={"force": True}, data=None) + kwargs = session.request.call_args.kwargs + assert kwargs["params"] == [("force", True)] + assert "data" not in kwargs + + def test_get_uses_query_branch_only(self): + """Verify GET only sends params and ignores data.""" + client, session = _make_client() + client._request("GET", ("v",), params={"q": "x"}, data={"ignored": True}) + kwargs = session.request.call_args.kwargs + assert kwargs["params"] == [("q", "x")] + assert "data" not in kwargs + + +class TestBaseResourceDelete: + """Test BaseResource.delete query/body routing to _APIPath.""" + + def test_passes_query_and_body(self): + """Verify query_params and body_params reach api[id].delete.""" + res = _make_resource() + delete_mock = MagicMock(return_value={}) + res.client.api.__getitem__.return_value.__getitem__.return_value.delete = delete_mock + + res.delete(42, query_params={"force": True}, body_params={"reason": "x"}) + + delete_mock.assert_called_once_with(_query_params={"force": True}, reason="x") + + def test_no_extras(self): + """Verify delete without extras calls api[id].delete().""" + res = _make_resource() + delete_mock = MagicMock(return_value=None) + res.client.api.__getitem__.return_value.__getitem__.return_value.delete = delete_mock + + result = res.delete(42) + delete_mock.assert_called_once_with() + assert result == {} + + def test_only_body(self): + """Verify body-only delete spreads body into kwargs.""" + res = _make_resource() + delete_mock = MagicMock(return_value={}) + res.client.api.__getitem__.return_value.__getitem__.return_value.delete = delete_mock + + res.delete(7, body_params={"reason": "x"}) + delete_mock.assert_called_once_with(reason="x") + + +class TestCollectDeleteParams: + """Test BaseResource._collect_delete_params.""" + + def test_splits_query_and_body(self): + """Verify params split per delete_query_params and delete_body_params.""" + res = _make_resource( + delete_query=["force"], + delete_body=["reason"], + params={"force": True, "reason": "x", "name": "foo"}, + ) + assert res._collect_delete_params() == {"query": {"force": True}, "body": {"reason": "x"}} + + def test_skips_none_values(self): + """Verify None values are excluded.""" + res = _make_resource( + delete_query=["force"], + delete_body=["reason"], + params={"force": None, "reason": None}, + ) + assert res._collect_delete_params() == {"query": {}, "body": {}} + + def test_missing_keys(self): + """Verify missing keys produce empty dicts.""" + res = _make_resource(delete_query=["force"], delete_body=["reason"], params={}) + assert res._collect_delete_params() == {"query": {}, "body": {}} + + def test_falsy_but_set_values_included(self): + """Verify False and 0 are kept (only None is filtered).""" + res = _make_resource( + delete_query=["force"], + delete_body=["count"], + params={"force": False, "count": 0}, + ) + assert res._collect_delete_params() == {"query": {"force": False}, "body": {"count": 0}} + + +class TestBuildDesiredStateExclusion: + """Test BaseResource.build_desired_state excludes DELETE-only and framework fields.""" + + def test_excludes_delete_query_and_body_params(self): + """Verify DELETE-only fields never appear in create/update payload.""" + res = _make_resource( + delete_query=["force"], + delete_body=["reason"], + params={ + "name": "foo", + "path": "/x", + "force": True, + "reason": "because", + "state": "present", + "vms": {"host": "h"}, + "wait": True, + }, + ) + assert res.build_desired_state(operation="update") == {"name": "foo", "path": "/x"} + + def test_excludes_framework_fields(self): + """Verify state/wait/id/query/wait_timeout are excluded.""" + res = _make_resource(params={"name": "foo", "id": 1, "state": "absent", "wait_timeout": 5, "query": "x"}) + assert res.build_desired_state() == {"name": "foo"} + + def test_skips_none_values(self): + """Verify None values are excluded from desired state.""" + res = _make_resource(params={"name": "foo", "path": None}) + assert res.build_desired_state() == {"name": "foo"} diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/__init__.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_dns.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_dns.py new file mode 100644 index 000000000..3752a1a7a --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_dns.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for dns module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"id": 1, "name": "test"} + overrides = get_overrides("dns") + result = normalize_resource(resource, overrides) + assert "id" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"name": "test-value"} + overrides = get_overrides("dns") + result = normalize_resource(resource, overrides) + assert "name" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"name": "test"} + desired = {"name": "test"} + overrides = get_overrides("dns") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"name": "old"} + desired = {"name": "new"} + overrides = get_overrides("dns") + patch = compute_patch(current, desired, overrides) + assert patch == {"name": "new"} + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_eventdefinitionconfigs.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_eventdefinitionconfigs.py new file mode 100644 index 000000000..ebd1015b1 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_eventdefinitionconfigs.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for eventdefinitionconfigs module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"id": 1, "smtp_host": "test"} + overrides = get_overrides("eventdefinitionconfigs") + result = normalize_resource(resource, overrides) + assert "id" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"smtp_host": "test-value"} + overrides = get_overrides("eventdefinitionconfigs") + result = normalize_resource(resource, overrides) + assert "smtp_host" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"smtp_host": "test"} + desired = {"smtp_host": "test"} + overrides = get_overrides("eventdefinitionconfigs") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"smtp_host": "old"} + desired = {"smtp_host": "new"} + overrides = get_overrides("eventdefinitionconfigs") + patch = compute_patch(current, desired, overrides) + assert patch == {"smtp_host": "new"} + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_globalsnapstreams.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_globalsnapstreams.py new file mode 100644 index 000000000..4642e6c9b --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_globalsnapstreams.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for globalsnapstreams module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"id": 1, "name": "test"} + overrides = get_overrides("globalsnapstreams") + result = normalize_resource(resource, overrides) + assert "id" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"name": "test-value"} + overrides = get_overrides("globalsnapstreams") + result = normalize_resource(resource, overrides) + assert "name" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"name": "test"} + desired = {"name": "test"} + overrides = get_overrides("globalsnapstreams") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"name": "old"} + desired = {"name": "new"} + overrides = get_overrides("globalsnapstreams") + patch = compute_patch(current, desired, overrides) + assert patch == {"name": "new"} + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_groups.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_groups.py new file mode 100644 index 000000000..d7119dc0f --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_groups.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for groups module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"id": 1, "name": "test"} + overrides = get_overrides("groups") + result = normalize_resource(resource, overrides) + assert "id" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"name": "test-value"} + overrides = get_overrides("groups") + result = normalize_resource(resource, overrides) + assert "name" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"name": "test"} + desired = {"name": "test"} + overrides = get_overrides("groups") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"name": "old"} + desired = {"name": "new"} + overrides = get_overrides("groups") + patch = compute_patch(current, desired, overrides) + assert patch == {"name": "new"} + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_ldaps.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_ldaps.py new file mode 100644 index 000000000..26a1546ec --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_ldaps.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for ldaps module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"id": 1, "name": "test"} + overrides = get_overrides("ldaps") + result = normalize_resource(resource, overrides) + assert "id" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"name": "test-value"} + overrides = get_overrides("ldaps") + result = normalize_resource(resource, overrides) + assert "name" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"name": "test"} + desired = {"name": "test"} + overrides = get_overrides("ldaps") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"name": "old"} + desired = {"name": "new"} + overrides = get_overrides("ldaps") + patch = compute_patch(current, desired, overrides) + assert patch == {"name": "new"} + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_nativereplicationremotetargets.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_nativereplicationremotetargets.py new file mode 100644 index 000000000..bed0b4f21 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_nativereplicationremotetargets.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for nativereplicationremotetargets module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"guid": 1, "name": "test"} + overrides = get_overrides("nativereplicationremotetargets") + result = normalize_resource(resource, overrides) + assert "guid" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"name": "test-value"} + overrides = get_overrides("nativereplicationremotetargets") + result = normalize_resource(resource, overrides) + assert "name" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"name": "test"} + desired = {"name": "test"} + overrides = get_overrides("nativereplicationremotetargets") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"name": "old"} + desired = {"name": "new"} + overrides = get_overrides("nativereplicationremotetargets") + patch = compute_patch(current, desired, overrides) + assert patch == {"name": "new"} + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_nonlocal_group.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_nonlocal_group.py new file mode 100644 index 000000000..ac67fcca5 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_nonlocal_group.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for nonlocal_group module.""" + +# BEGIN AUTOGENERATED TESTS +from ansible_collections.vastdata.vms.plugins.modules import nonlocal_group + + +class TestModuleStructure: + """Test module structure and documentation.""" + + def test_module_imports(self): + """Test that the module can be imported.""" + assert nonlocal_group is not None + assert hasattr(nonlocal_group, "main") + + def test_has_documentation(self): + """Test that the module has DOCUMENTATION.""" + assert hasattr(nonlocal_group, "DOCUMENTATION") + assert len(nonlocal_group.DOCUMENTATION) > 0 + + def test_has_examples(self): + """Test that the module has EXAMPLES.""" + assert hasattr(nonlocal_group, "EXAMPLES") + assert len(nonlocal_group.EXAMPLES) > 0 + + def test_has_return(self): + """Test that the module has RETURN documentation.""" + assert hasattr(nonlocal_group, "RETURN") + assert len(nonlocal_group.RETURN) > 0 + + def test_has_argument_spec(self): + """Test that the module has ARGUMENT_SPEC.""" + assert hasattr(nonlocal_group, "ARGUMENT_SPEC") + assert isinstance(nonlocal_group.ARGUMENT_SPEC, dict) + + +class TestConnectionParams: + """Test connection parameters in ARGUMENT_SPEC.""" + + def test_has_vms_dict(self): + """Test vms parameter dict is present and required.""" + spec = nonlocal_group.ARGUMENT_SPEC + assert "vms" in spec + assert spec["vms"]["required"] is True + assert spec["vms"]["type"] == "dict" + + def test_has_host(self): + """Test host parameter is required within vms.""" + spec = nonlocal_group.ARGUMENT_SPEC + assert "vms" in spec + vms_options = spec["vms"]["options"] + assert "host" in vms_options + assert vms_options["host"]["required"] is True + + def test_has_auth_params(self): + """Test authentication parameters exist within vms.""" + spec = nonlocal_group.ARGUMENT_SPEC + assert "vms" in spec + vms_options = spec["vms"]["options"] + assert "token" in vms_options + assert "username" in vms_options + assert "password" in vms_options + + +class TestSubEndpointSpec: + """Test sub-endpoint ARGUMENT_SPEC entries.""" + + def test_has_state_param(self): + """Test state parameter with present/absent choices.""" + spec = nonlocal_group.ARGUMENT_SPEC + assert "state" in spec + assert spec["state"]["choices"] == ["present", "absent"] + assert spec["state"]["default"] == "present" + + def test_has_body_params(self): + """Test body parameters are present.""" + spec = nonlocal_group.ARGUMENT_SPEC + expected = ["gid", "groupname", "s3_policies_ids", "sid", "tenant_id", "vaid"] + for param in expected: + assert param in spec, f"Missing param: {param}" + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_nonlocal_user.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_nonlocal_user.py new file mode 100644 index 000000000..fe1b0cf0f --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_nonlocal_user.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for nonlocal_user module.""" + +# BEGIN AUTOGENERATED TESTS +from ansible_collections.vastdata.vms.plugins.modules import nonlocal_user + + +class TestModuleStructure: + """Test module structure and documentation.""" + + def test_module_imports(self): + """Test that the module can be imported.""" + assert nonlocal_user is not None + assert hasattr(nonlocal_user, "main") + + def test_has_documentation(self): + """Test that the module has DOCUMENTATION.""" + assert hasattr(nonlocal_user, "DOCUMENTATION") + assert len(nonlocal_user.DOCUMENTATION) > 0 + + def test_has_examples(self): + """Test that the module has EXAMPLES.""" + assert hasattr(nonlocal_user, "EXAMPLES") + assert len(nonlocal_user.EXAMPLES) > 0 + + def test_has_return(self): + """Test that the module has RETURN documentation.""" + assert hasattr(nonlocal_user, "RETURN") + assert len(nonlocal_user.RETURN) > 0 + + def test_has_argument_spec(self): + """Test that the module has ARGUMENT_SPEC.""" + assert hasattr(nonlocal_user, "ARGUMENT_SPEC") + assert isinstance(nonlocal_user.ARGUMENT_SPEC, dict) + + +class TestConnectionParams: + """Test connection parameters in ARGUMENT_SPEC.""" + + def test_has_vms_dict(self): + """Test vms parameter dict is present and required.""" + spec = nonlocal_user.ARGUMENT_SPEC + assert "vms" in spec + assert spec["vms"]["required"] is True + assert spec["vms"]["type"] == "dict" + + def test_has_host(self): + """Test host parameter is required within vms.""" + spec = nonlocal_user.ARGUMENT_SPEC + assert "vms" in spec + vms_options = spec["vms"]["options"] + assert "host" in vms_options + assert vms_options["host"]["required"] is True + + def test_has_auth_params(self): + """Test authentication parameters exist within vms.""" + spec = nonlocal_user.ARGUMENT_SPEC + assert "vms" in spec + vms_options = spec["vms"]["options"] + assert "token" in vms_options + assert "username" in vms_options + assert "password" in vms_options + + +class TestSubEndpointSpec: + """Test sub-endpoint ARGUMENT_SPEC entries.""" + + def test_has_state_param(self): + """Test state parameter with present/absent choices.""" + spec = nonlocal_user.ARGUMENT_SPEC + assert "state" in spec + assert spec["state"]["choices"] == ["present", "absent"] + assert spec["state"]["default"] == "present" + + def test_has_body_params(self): + """Test body parameters are present.""" + spec = nonlocal_user.ARGUMENT_SPEC + expected = [ + "allow_create_bucket", + "allow_delete_bucket", + "login_name", + "s3_policies_ids", + "s3_superuser", + "sid", + "tenant_id", + "uid", + "username", + ] + for param in expected: + assert param in spec, f"Missing param: {param}" + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_protectedpaths.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_protectedpaths.py new file mode 100644 index 000000000..3595818c5 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_protectedpaths.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for protectedpaths module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"id": 1, "name": "test"} + overrides = get_overrides("protectedpaths") + result = normalize_resource(resource, overrides) + assert "id" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"name": "test-value"} + overrides = get_overrides("protectedpaths") + result = normalize_resource(resource, overrides) + assert "name" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"name": "test"} + desired = {"name": "test"} + overrides = get_overrides("protectedpaths") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"name": "old"} + desired = {"name": "new"} + overrides = get_overrides("protectedpaths") + patch = compute_patch(current, desired, overrides) + assert patch == {"name": "new"} + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_protectionpolicies.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_protectionpolicies.py new file mode 100644 index 000000000..7e362527b --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_protectionpolicies.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for protectionpolicies module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"id": 1, "name": "test"} + overrides = get_overrides("protectionpolicies") + result = normalize_resource(resource, overrides) + assert "id" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"name": "test-value"} + overrides = get_overrides("protectionpolicies") + result = normalize_resource(resource, overrides) + assert "name" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"name": "test"} + desired = {"name": "test"} + overrides = get_overrides("protectionpolicies") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"name": "old"} + desired = {"name": "new"} + overrides = get_overrides("protectionpolicies") + patch = compute_patch(current, desired, overrides) + assert patch == {"name": "new"} + + +# END AUTOGENERATED TESTS + + +class TestFramesIdempotency: + """Verify frames field comparison is unit- and default-aware.""" + + overrides = get_overrides("protectionpolicies") + + def _patch_for(self, current_frames, desired_frames): + current = {"frames": current_frames, "name": "p"} + desired = {"frames": desired_frames, "name": "p"} + current_n = normalize_resource(current, self.overrides, exclude_immutable=True, user_resource=desired) + desired_n = normalize_resource(desired, self.overrides, exclude_immutable=True) + return compute_patch(current_n, desired_n, self.overrides) + + def test_unit_normalization_7D_equals_1W(self): + """Bug reproducer: server returns '1W' when user wrote '7D'.""" + current = [ + {"every": "1W", "start-at": "2025-08-01 09:00:00", "keep-local": "2W", "keep-remote": "0s"}, + {"every": "1W", "start-at": "2025-08-01 09:00:00", "keep-local": "8D", "keep-remote": "0s"}, + ] + desired = [ + {"every": "7D", "keep-local": "14D", "keep-remote": "0s", "start-at": "2025-08-01 09:00:00"}, + {"every": "7D", "keep-local": "8D", "keep-remote": "0s", "start-at": "2025-08-01 09:00:00"}, + ] + assert self._patch_for(current, desired) == {} + + def test_unit_normalization_24H_equals_1D(self): + """Server normalizes 24H -> 1D.""" + current = [{"every": "1D", "keep-local": "1W", "keep-remote": "0s"}] + desired = [{"every": "24H", "keep-local": "7D", "keep-remote": "0s"}] + assert self._patch_for(current, desired) == {} + + def test_default_keep_remote_dropped(self): + """User omits keep-remote; server adds 'keep-remote: 0s'. No diff.""" + current = [{"every": "7D", "keep-local": "14D", "keep-remote": "0s", "start-at": "2025-08-01 09:00:00"}] + desired = [{"every": "7D", "keep-local": "14D", "start-at": "2025-08-01 09:00:00"}] + assert self._patch_for(current, desired) == {} + + def test_real_change_is_detected(self): + """Genuine differences must still produce a patch.""" + current = [{"every": "7D", "keep-local": "14D", "keep-remote": "0s"}] + desired = [{"every": "7D", "keep-local": "30D", "keep-remote": "0s"}] + patch = self._patch_for(current, desired) + assert "frames" in patch + assert patch["frames"][0]["keep-local"] == "30D" + + def test_different_frame_count_produces_patch(self): + """Adding/removing a frame must trigger a change.""" + current = [{"every": "7D", "keep-local": "14D", "keep-remote": "0s"}] + desired = [ + {"every": "7D", "keep-local": "14D", "keep-remote": "0s"}, + {"every": "7D", "keep-local": "8D", "keep-remote": "0s"}, + ] + assert "frames" in self._patch_for(current, desired) diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_quotas.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_quotas.py new file mode 100644 index 000000000..2dacee231 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_quotas.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for quotas module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"id": 1, "path": "test"} + overrides = get_overrides("quotas") + result = normalize_resource(resource, overrides) + assert "id" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"path": "test-value"} + overrides = get_overrides("quotas") + result = normalize_resource(resource, overrides) + assert "path" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"path": "test"} + desired = {"path": "test"} + overrides = get_overrides("quotas") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"path": "old"} + desired = {"path": "new"} + overrides = get_overrides("quotas") + patch = compute_patch(current, desired, overrides) + assert patch == {"path": "new"} + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_s3policies.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_s3policies.py new file mode 100644 index 000000000..60618d376 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_s3policies.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for s3policies module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"id": 1, "name": "test"} + overrides = get_overrides("s3policies") + result = normalize_resource(resource, overrides) + assert "id" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"name": "test-value"} + overrides = get_overrides("s3policies") + result = normalize_resource(resource, overrides) + assert "name" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"name": "test"} + desired = {"name": "test"} + overrides = get_overrides("s3policies") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"name": "old"} + desired = {"name": "new"} + overrides = get_overrides("s3policies") + patch = compute_patch(current, desired, overrides) + assert patch == {"name": "new"} + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_snapshots.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_snapshots.py new file mode 100644 index 000000000..2bd8088f6 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_snapshots.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for snapshots module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"id": 1, "name": "test"} + overrides = get_overrides("snapshots") + result = normalize_resource(resource, overrides) + assert "id" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"name": "test-value"} + overrides = get_overrides("snapshots") + result = normalize_resource(resource, overrides) + assert "name" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"name": "test"} + desired = {"name": "test"} + overrides = get_overrides("snapshots") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"name": "old"} + desired = {"name": "new"} + overrides = get_overrides("snapshots") + patch = compute_patch(current, desired, overrides) + assert patch == {"name": "new"} + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_tenants.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_tenants.py new file mode 100644 index 000000000..59d69930a --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_tenants.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for tenants module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"id": 1, "name": "test"} + overrides = get_overrides("tenants") + result = normalize_resource(resource, overrides) + assert "id" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"name": "test-value"} + overrides = get_overrides("tenants") + result = normalize_resource(resource, overrides) + assert "name" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"name": "test"} + desired = {"name": "test"} + overrides = get_overrides("tenants") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"name": "old"} + desired = {"name": "new"} + overrides = get_overrides("tenants") + patch = compute_patch(current, desired, overrides) + assert patch == {"name": "new"} + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_user_key.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_user_key.py new file mode 100644 index 000000000..e034002f8 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_user_key.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for user_key module.""" + +# BEGIN AUTOGENERATED TESTS +from ansible_collections.vastdata.vms.plugins.modules import user_key + + +class TestModuleStructure: + """Test module structure and documentation.""" + + def test_module_imports(self): + """Test that the module can be imported.""" + assert user_key is not None + assert hasattr(user_key, "main") + + def test_has_documentation(self): + """Test that the module has DOCUMENTATION.""" + assert hasattr(user_key, "DOCUMENTATION") + assert len(user_key.DOCUMENTATION) > 0 + + def test_has_examples(self): + """Test that the module has EXAMPLES.""" + assert hasattr(user_key, "EXAMPLES") + assert len(user_key.EXAMPLES) > 0 + + def test_has_return(self): + """Test that the module has RETURN documentation.""" + assert hasattr(user_key, "RETURN") + assert len(user_key.RETURN) > 0 + + def test_has_argument_spec(self): + """Test that the module has ARGUMENT_SPEC.""" + assert hasattr(user_key, "ARGUMENT_SPEC") + assert isinstance(user_key.ARGUMENT_SPEC, dict) + + +class TestConnectionParams: + """Test connection parameters in ARGUMENT_SPEC.""" + + def test_has_vms_dict(self): + """Test vms parameter dict is present and required.""" + spec = user_key.ARGUMENT_SPEC + assert "vms" in spec + assert spec["vms"]["required"] is True + assert spec["vms"]["type"] == "dict" + + def test_has_host(self): + """Test host parameter is required within vms.""" + spec = user_key.ARGUMENT_SPEC + assert "vms" in spec + vms_options = spec["vms"]["options"] + assert "host" in vms_options + assert vms_options["host"]["required"] is True + + def test_has_auth_params(self): + """Test authentication parameters exist within vms.""" + spec = user_key.ARGUMENT_SPEC + assert "vms" in spec + vms_options = spec["vms"]["options"] + assert "token" in vms_options + assert "username" in vms_options + assert "password" in vms_options + + +class TestSubEndpointSpec: + """Test sub-endpoint ARGUMENT_SPEC entries.""" + + def test_has_parent_id_param(self): + """Test parent ID parameter is present and required.""" + spec = user_key.ARGUMENT_SPEC + assert "user_id" in spec + assert spec["user_id"]["required"] is True + assert spec["user_id"]["type"] == "int" + + def test_has_state_param(self): + """Test state parameter with present/absent choices.""" + spec = user_key.ARGUMENT_SPEC + assert "state" in spec + assert spec["state"]["choices"] == ["present", "absent"] + assert spec["state"]["default"] == "present" + + def test_has_body_params(self): + """Test body parameters are present.""" + spec = user_key.ARGUMENT_SPEC + expected = ["access_key", "enabled", "tenant_id"] + for param in expected: + assert param in spec, f"Missing param: {param}" + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_users.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_users.py new file mode 100644 index 000000000..a526990b0 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_users.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for users module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"id": 1, "name": "test"} + overrides = get_overrides("users") + result = normalize_resource(resource, overrides) + assert "id" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"name": "test-value"} + overrides = get_overrides("users") + result = normalize_resource(resource, overrides) + assert "name" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"name": "test"} + desired = {"name": "test"} + overrides = get_overrides("users") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"name": "old"} + desired = {"name": "new"} + overrides = get_overrides("users") + patch = compute_patch(current, desired, overrides) + assert patch == {"name": "new"} + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_viewpolicies.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_viewpolicies.py new file mode 100644 index 000000000..74cdbd6fe --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_viewpolicies.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for viewpolicies module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"id": 1, "name": "test"} + overrides = get_overrides("viewpolicies") + result = normalize_resource(resource, overrides) + assert "id" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"name": "test-value"} + overrides = get_overrides("viewpolicies") + result = normalize_resource(resource, overrides) + assert "name" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"name": "test"} + desired = {"name": "test"} + overrides = get_overrides("viewpolicies") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"name": "old"} + desired = {"name": "new"} + overrides = get_overrides("viewpolicies") + patch = compute_patch(current, desired, overrides) + assert patch == {"name": "new"} + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_views.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_views.py new file mode 100644 index 000000000..fe7914b2f --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_views.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for views module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"id": 1, "path": "test"} + overrides = get_overrides("views") + result = normalize_resource(resource, overrides) + assert "id" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"path": "test-value"} + overrides = get_overrides("views") + result = normalize_resource(resource, overrides) + assert "path" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"path": "test"} + desired = {"path": "test"} + overrides = get_overrides("views") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"path": "old"} + desired = {"path": "new"} + overrides = get_overrides("views") + patch = compute_patch(current, desired, overrides) + assert patch == {"path": "new"} + + +# END AUTOGENERATED TESTS diff --git a/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_vippools.py b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_vippools.py new file mode 100644 index 000000000..0ed73b862 --- /dev/null +++ b/vendor/ansible_collections/vastdata/vms/tests/unit/plugins/modules/test_vippools.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Unit tests for vippools module.""" + +# BEGIN AUTOGENERATED TESTS +import sys +from pathlib import Path + +# Add collection root to path for importing module_utils +collection_root = Path(__file__).parent.parent.parent.parent.parent +sys.path.insert(0, str(collection_root)) + +from plugins.module_utils.vast.diff import ( + compute_patch, + normalize_resource, +) +from plugins.module_utils.vast.schema_overrides import ( + get_overrides, +) + + +class TestNormalize: + """Test normalize_resource function.""" + + def test_normalize_removes_read_only_fields(self): + """Verify read-only fields are removed.""" + resource = {"id": 1, "name": "test"} + overrides = get_overrides("vippools") + result = normalize_resource(resource, overrides) + assert "id" not in result + + def test_normalize_preserves_writable_fields(self): + """Verify writable fields are preserved.""" + resource = {"name": "test-value"} + overrides = get_overrides("vippools") + result = normalize_resource(resource, overrides) + assert "name" in result + + +class TestComputePatch: + """Test compute_patch function.""" + + def test_no_changes(self): + """Verify no patch when values match.""" + current = {"name": "test"} + desired = {"name": "test"} + overrides = get_overrides("vippools") + patch = compute_patch(current, desired, overrides) + assert patch == {} + + def test_detects_change(self): + """Verify patch detects changes.""" + current = {"name": "old"} + desired = {"name": "new"} + overrides = get_overrides("vippools") + patch = compute_patch(current, desired, overrides) + assert patch == {"name": "new"} + + +# END AUTOGENERATED TESTS