From 444cd7b6a1f8f272b61dbedeb5e957e380208f37 Mon Sep 17 00:00:00 2001 From: Jagadeesh N V Date: Thu, 12 Mar 2026 13:48:10 +0530 Subject: [PATCH 01/10] Propdal for new generic storage --- docs/cloud_init_mounts_ansible_hld.md | 905 ++++++++++++++++++++++++++ docs/cloud_init_mounts_python_hld.md | 509 +++++++++++++++ docs/logos/Liqid.png | Bin 27613 -> 0 bytes docs/logos/delltech.jpg | Bin 244754 -> 0 bytes docs/logos/omnia-logo-transparent.png | Bin 33093 -> 0 bytes docs/logos/omnia-logo.png | Bin 27559 -> 0 bytes docs/logos/pisa.png | Bin 24727 -> 0 bytes input/storage_config.yml | 116 ++++ 8 files changed, 1530 insertions(+) create mode 100644 docs/cloud_init_mounts_ansible_hld.md create mode 100644 docs/cloud_init_mounts_python_hld.md delete mode 100644 docs/logos/Liqid.png delete mode 100644 docs/logos/delltech.jpg delete mode 100644 docs/logos/omnia-logo-transparent.png delete mode 100644 docs/logos/omnia-logo.png delete mode 100644 docs/logos/pisa.png diff --git a/docs/cloud_init_mounts_ansible_hld.md b/docs/cloud_init_mounts_ansible_hld.md new file mode 100644 index 0000000000..19ac923c43 --- /dev/null +++ b/docs/cloud_init_mounts_ansible_hld.md @@ -0,0 +1,905 @@ +# High-Level Design: Cloud-Init Mounts Configuration (Ansible Implementation) + +## Overview + +This document describes an Ansible-based design for a flexible, cloud-init compatible mount and swap configuration system. The implementation uses Ansible roles and Jinja2 templates to manage storage mounts and swap files with fine-grained control over which nodes receive each configuration. + +**Key Constraint**: The Ansible controller does not have direct access to the NFS server or storage devices. All operations are executed on target nodes. + +## Design Goals + +1. **Ansible-Native**: Use Ansible roles, tasks, and Jinja2 templates (no Python modules) +2. **Cloud-Init Compatibility**: Generate configurations compatible with cloud-init's mounts module +3. **Granular Control**: Target specific nodes by roles, hostnames, or groups from pxe_mapping.csv +4. **Idempotency**: Support repeated executions without side effects +5. **No Controller Dependencies**: All storage operations execute on target nodes + +## Architecture + +### 1. Configuration Input Structure + +The system accepts configuration from `storage_config.yml`: + +#### 1.1 Mounts Configuration + +```yaml +mounts: + - name: "nfs_slurm_home" # Unique identifier + fs_spec: "172.16.107.168:/mnt/share/omnia" + fs_file: "/home" + fs_vfstype: "nfs" + fs_mntops: "defaults,nofail,_netdev" + fs_freq: "0" + fs_passno: "0" + roles: ["slurm_control_node", "slurm_node"] + hostnames: [] + groups: [] + + - name: "local_data" + fs_spec: "/dev/sdc" + fs_file: "/opt/data" + fs_vfstype: "ext4" + fs_mntops: "defaults,nofail" + fs_freq: "0" + fs_passno: "2" + roles: [] + hostnames: [] + groups: ["grp1"] +``` + +#### 1.2 Swap Configuration + +```yaml +swap: + - name: "compute_swap" + filename: "/swapfile" + size: "4G" + maxsize: "8G" + roles: ["slurm_node"] + hostnames: [] + groups: [] +``` + +#### 1.3 Mount Default Fields + +```yaml +mount_default_fields: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] +``` + +### 2. Ansible Role Structure + +``` +storage_generic/ +├── roles/ +│ ├── storage_mounts/ +│ │ ├── tasks/ +│ │ │ ├── main.yml +│ │ │ ├── parse_pxe_mapping.yml +│ │ │ ├── resolve_targets.yml +│ │ │ ├── generate_cloud_init.yml +│ │ │ └── apply_mounts.yml +│ │ ├── templates/ +│ │ │ ├── cloud_init_mounts.yml.j2 +│ │ │ └── fstab_entry.j2 +│ │ ├── vars/ +│ │ │ └── main.yml +│ │ └── defaults/ +│ │ └── main.yml +│ └── nfs_client/ +│ └── (existing role - can be extended) +├── input/ +│ └── storage_config.yml +└── playbooks/ + └── configure_storage_mounts.yml +``` + +### 3. Target Resolution Process (Ansible Implementation) + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Stage 1: Load Configuration (Ansible Controller) │ +│ - include_vars: storage_config.yml │ +│ - Read pxe_mapping_file.csv using lookup('file') │ +│ - Parse CSV using community.general.read_csv │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Stage 2: Build PXE Mapping Dictionary (set_fact) │ +│ - Create hostname → attributes mapping │ +│ - Create role → hostnames mapping │ +│ - Create group → hostnames mapping │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Stage 3: Resolve Targets for Each Mount/Swap (Jinja2) │ +│ - For each mount in mounts[] │ +│ - For each swap in swap[] │ +│ - Apply role/hostname/group filters │ +│ - Build unique hostname list per mount/swap │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Stage 4: Build Hostname-to-Mounts Mapping (set_fact) │ +│ - Invert mount→hosts to host→mounts mapping │ +│ - Each hostname gets list of applicable mount names │ +│ - Each hostname gets list of applicable swap names │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Stage 5: Generate Cloud-Init Config (delegate_to) │ +│ - For each target host (delegate_to: {{ item }}) │ +│ - Use Jinja2 template to generate cloud-init YAML │ +│ - Write to /etc/cloud/cloud.cfg.d/99-mounts.cfg │ +│ - OR update /etc/fstab directly │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Stage 6: Apply Configuration (on target nodes) │ +│ - Run cloud-init modules apply mounts │ +│ - OR use ansible.posix.mount module directly │ +│ - Create swap files using command/shell module │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 4. Hostname Resolution Algorithm (Ansible/Jinja2) + +#### 4.1 PXE Mapping File Structure + +The `pxe_mapping_file.csv` is the source of truth: + +```csv +HOSTNAME,MAC,IP,SERVICE_TAG,GROUP_NAME,NODE_ROLE +manager01,aa:bb:cc:dd:ee:01,192.168.1.10,SVC001,grp0,slurm_control_node +compute01,aa:bb:cc:dd:ee:02,192.168.1.11,SVC002,grp1,slurm_node +compute02,aa:bb:cc:dd:ee:03,192.168.1.12,SVC003,grp1,slurm_node +compute03,aa:bb:cc:dd:ee:04,192.168.1.13,SVC004,grp2,slurm_node +``` + +#### 4.2 Parse PXE Mapping (Ansible Task) + +```yaml +# tasks/parse_pxe_mapping.yml +--- +- name: Read PXE mapping file + set_fact: + pxe_mapping_raw: "{{ lookup('file', pxe_mapping_file_path) }}" + +- name: Parse CSV to list of dictionaries + set_fact: + pxe_mapping_list: "{{ pxe_mapping_raw | community.general.read_csv }}" + +- name: Build hostname to attributes mapping + set_fact: + pxe_hostname_map: >- + {{ + pxe_hostname_map | default({}) | combine({ + item.HOSTNAME: { + 'ip': item.IP, + 'mac': item.MAC, + 'role': item.NODE_ROLE, + 'group': item.GROUP_NAME, + 'service_tag': item.SERVICE_TAG + } + }) + }} + loop: "{{ pxe_mapping_list }}" + loop_control: + label: "{{ item.HOSTNAME }}" + +- name: Build role to hostnames mapping + set_fact: + pxe_role_map: >- + {{ + pxe_role_map | default({}) | combine({ + item.NODE_ROLE: (pxe_role_map[item.NODE_ROLE] | default([])) + [item.HOSTNAME] + }) + }} + loop: "{{ pxe_mapping_list }}" + loop_control: + label: "{{ item.HOSTNAME }}" + +- name: Build group to hostnames mapping + set_fact: + pxe_group_map: >- + {{ + pxe_group_map | default({}) | combine({ + item.GROUP_NAME: (pxe_group_map[item.GROUP_NAME] | default([])) + [item.HOSTNAME] + }) + }} + loop: "{{ pxe_mapping_list }}" + loop_control: + label: "{{ item.HOSTNAME }}" + +- name: Get all hostnames + set_fact: + all_hostnames: "{{ pxe_mapping_list | map(attribute='HOSTNAME') | list }}" +``` + +#### 4.3 Resolve Targets for Each Mount (Ansible Task) + +```yaml +# tasks/resolve_targets.yml +--- +- name: Resolve target hostnames for each mount + set_fact: + mount_targets: >- + {{ + mount_targets | default({}) | combine({ + item.name: resolve_mount_targets(item, pxe_role_map, pxe_group_map, all_hostnames) + }) + }} + loop: "{{ mounts }}" + loop_control: + label: "{{ item.name }}" + vars: + resolve_mount_targets: | + {% set targets = [] %} + {% set mount = item %} + + {# If all targeting fields are empty, use all hostnames #} + {% if not mount.roles and not mount.hostnames and not mount.groups %} + {% set targets = all_hostnames %} + {% else %} + {# Resolve roles to hostnames #} + {% for role in mount.roles | default([]) %} + {% if role in pxe_role_map %} + {% set targets = targets + pxe_role_map[role] %} + {% endif %} + {% endfor %} + + {# Add explicit hostnames #} + {% set targets = targets + (mount.hostnames | default([])) %} + + {# Resolve groups to hostnames #} + {% for group in mount.groups | default([]) %} + {% if group in pxe_group_map %} + {% set targets = targets + pxe_group_map[group] %} + {% endif %} + {% endfor %} + {% endif %} + + {# Return unique list #} + {{ targets | unique | list }} + +- name: Resolve target hostnames for each swap + set_fact: + swap_targets: >- + {{ + swap_targets | default({}) | combine({ + item.name: resolve_swap_targets(item, pxe_role_map, pxe_group_map, all_hostnames) + }) + }} + loop: "{{ swap }}" + loop_control: + label: "{{ item.name }}" + vars: + resolve_swap_targets: | + {# Same logic as mount resolution #} + {% set targets = [] %} + {% set swap_item = item %} + + {% if not swap_item.roles and not swap_item.hostnames and not swap_item.groups %} + {% set targets = all_hostnames %} + {% else %} + {% for role in swap_item.roles | default([]) %} + {% if role in pxe_role_map %} + {% set targets = targets + pxe_role_map[role] %} + {% endif %} + {% endfor %} + {% set targets = targets + (swap_item.hostnames | default([])) %} + {% for group in swap_item.groups | default([]) %} + {% if group in pxe_group_map %} + {% set targets = targets + pxe_group_map[group] %} + {% endif %} + {% endfor %} + {% endif %} + + {{ targets | unique | list }} +``` + +#### 4.4 Build Hostname-to-Mounts Mapping (Ansible Task) + +```yaml +# tasks/resolve_targets.yml (continued) +--- +- name: Build hostname to mounts mapping + set_fact: + hostname_mounts: >- + {{ + hostname_mounts | default({}) | combine({ + hostname: { + 'mounts': get_mounts_for_host(hostname, mount_targets, mounts), + 'swap': get_swap_for_host(hostname, swap_targets, swap) + } + }) + }} + loop: "{{ all_hostnames }}" + loop_control: + loop_var: hostname + vars: + get_mounts_for_host: | + {% set host_mounts = [] %} + {% for mount_name, target_hosts in mount_targets.items() %} + {% if hostname in target_hosts %} + {% set mount_config = mounts | selectattr('name', 'equalto', mount_name) | first %} + {% set host_mounts = host_mounts + [mount_config] %} + {% endif %} + {% endfor %} + {{ host_mounts }} + + get_swap_for_host: | + {% set host_swaps = [] %} + {% for swap_name, target_hosts in swap_targets.items() %} + {% if hostname in target_hosts %} + {% set swap_config = swap | selectattr('name', 'equalto', swap_name) | first %} + {% set host_swaps = host_swaps + [swap_config] %} + {% endif %} + {% endfor %} + {{ host_swaps }} +``` + +### 5. Cloud-Init Configuration Generation + +#### 5.1 Jinja2 Template for Cloud-Init + +```jinja2 +{# templates/cloud_init_mounts.yml.j2 #} +#cloud-config +# Generated by Omnia storage_mounts role +# Hostname: {{ inventory_hostname }} +# Generated at: {{ ansible_date_time.iso8601 }} + +{% if hostname_mounts[inventory_hostname].mounts | length > 0 %} +mounts: +{% for mount in hostname_mounts[inventory_hostname].mounts %} + - ["{{ mount.fs_spec }}", "{{ mount.fs_file }}", "{{ mount.fs_vfstype | default('auto') }}", "{{ mount.fs_mntops | default('defaults,nofail') }}", "{{ mount.fs_freq | default('0') }}", "{{ mount.fs_passno | default('2') }}"] +{% endfor %} + +mount_default_fields: {{ mount_default_fields | to_json }} +{% endif %} + +{% if hostname_mounts[inventory_hostname].swap | length > 0 %} +{% set swap_config = hostname_mounts[inventory_hostname].swap[0] %} +swap: + filename: {{ swap_config.filename }} + size: {{ swap_config.size }} +{% if swap_config.maxsize %} + maxsize: {{ swap_config.maxsize }} +{% endif %} +{% endif %} +``` + +#### 5.2 Generate and Apply Cloud-Init Configuration + +```yaml +# tasks/generate_cloud_init.yml +--- +- name: Create cloud-init configuration directory + file: + path: /etc/cloud/cloud.cfg.d + state: directory + mode: '0755' + delegate_to: "{{ item }}" + loop: "{{ all_hostnames }}" + when: hostname_mounts[item].mounts | length > 0 or hostname_mounts[item].swap | length > 0 + +- name: Generate cloud-init mounts configuration + template: + src: cloud_init_mounts.yml.j2 + dest: /etc/cloud/cloud.cfg.d/99-mounts.cfg + mode: '0644' + delegate_to: "{{ item }}" + loop: "{{ all_hostnames }}" + when: hostname_mounts[item].mounts | length > 0 or hostname_mounts[item].swap | length > 0 + notify: Apply cloud-init mounts + +- name: Apply cloud-init mounts module + command: cloud-init single --name mounts + delegate_to: "{{ item }}" + loop: "{{ all_hostnames }}" + when: + - hostname_mounts[item].mounts | length > 0 or hostname_mounts[item].swap | length > 0 + - apply_cloud_init | default(true) +``` + +### 6. Alternative: Direct Mount Management (Without Cloud-Init) + +For environments where cloud-init is not available or preferred: + +```yaml +# tasks/apply_mounts.yml +--- +- name: Mount filesystems directly using ansible.posix.mount + ansible.posix.mount: + path: "{{ mount_item.fs_file }}" + src: "{{ mount_item.fs_spec }}" + fstype: "{{ mount_item.fs_vfstype | default('auto') }}" + opts: "{{ mount_item.fs_mntops | default('defaults,nofail') }}" + dump: "{{ mount_item.fs_freq | default('0') }}" + passno: "{{ mount_item.fs_passno | default('2') }}" + state: mounted + delegate_to: "{{ hostname }}" + loop: "{{ hostname_mounts[hostname].mounts }}" + loop_control: + loop_var: mount_item + label: "{{ mount_item.name }}" + when: hostname_mounts[hostname].mounts | length > 0 + with_items: "{{ all_hostnames }}" + loop_control: + loop_var: hostname + +- name: Create swap file + command: | + dd if=/dev/zero of={{ swap_item.filename }} bs=1M count={{ swap_item.size | regex_replace('[^0-9]', '') }} + args: + creates: "{{ swap_item.filename }}" + delegate_to: "{{ hostname }}" + loop: "{{ hostname_mounts[hostname].swap }}" + loop_control: + loop_var: swap_item + label: "{{ swap_item.name }}" + when: hostname_mounts[hostname].swap | length > 0 + with_items: "{{ all_hostnames }}" + loop_control: + loop_var: hostname + +- name: Set swap file permissions + file: + path: "{{ swap_item.filename }}" + mode: '0600' + delegate_to: "{{ hostname }}" + loop: "{{ hostname_mounts[hostname].swap }}" + loop_control: + loop_var: swap_item + label: "{{ swap_item.name }}" + when: hostname_mounts[hostname].swap | length > 0 + with_items: "{{ all_hostnames }}" + loop_control: + loop_var: hostname + +- name: Format swap file + command: mkswap {{ swap_item.filename }} + delegate_to: "{{ hostname }}" + loop: "{{ hostname_mounts[hostname].swap }}" + loop_control: + loop_var: swap_item + label: "{{ swap_item.name }}" + when: hostname_mounts[hostname].swap | length > 0 + with_items: "{{ all_hostnames }}" + loop_control: + loop_var: hostname + +- name: Enable swap + command: swapon {{ swap_item.filename }} + delegate_to: "{{ hostname }}" + loop: "{{ hostname_mounts[hostname].swap }}" + loop_control: + loop_var: swap_item + label: "{{ swap_item.name }}" + when: hostname_mounts[hostname].swap | length > 0 + with_items: "{{ all_hostnames }}" + loop_control: + loop_var: hostname + +- name: Add swap to /etc/fstab + lineinfile: + path: /etc/fstab + line: "{{ swap_item.filename }} none swap sw 0 0" + state: present + delegate_to: "{{ hostname }}" + loop: "{{ hostname_mounts[hostname].swap }}" + loop_control: + loop_var: swap_item + label: "{{ swap_item.name }}" + when: hostname_mounts[hostname].swap | length > 0 + with_items: "{{ all_hostnames }}" + loop_control: + loop_var: hostname +``` + +### 7. Main Playbook + +```yaml +# playbooks/configure_storage_mounts.yml +--- +- name: Configure Storage Mounts and Swap + hosts: localhost + gather_facts: false + vars_files: + - ../input/storage_config.yml + vars: + pxe_mapping_file_path: "/path/to/pxe_mapping_file.csv" + apply_cloud_init: true # Set to false to use direct mount management + + tasks: + - name: Parse PXE mapping file + include_tasks: ../roles/storage_mounts/tasks/parse_pxe_mapping.yml + + - name: Resolve mount and swap targets + include_tasks: ../roles/storage_mounts/tasks/resolve_targets.yml + + - name: Display hostname to mounts mapping + debug: + var: hostname_mounts + verbosity: 1 + + - name: Generate and apply cloud-init configuration + include_tasks: ../roles/storage_mounts/tasks/generate_cloud_init.yml + when: apply_cloud_init | default(true) + + - name: Apply mounts directly (without cloud-init) + include_tasks: ../roles/storage_mounts/tasks/apply_mounts.yml + when: not (apply_cloud_init | default(true)) +``` + +### 8. Data Flow Example + +#### Input Configuration + +```yaml +# storage_config.yml +mounts: + - name: "nfs_slurm_home" + fs_spec: "172.16.107.168:/mnt/share/omnia" + fs_file: "/home" + fs_vfstype: "nfs" + fs_mntops: "defaults,nofail,_netdev" + fs_freq: "0" + fs_passno: "0" + roles: ["slurm_control_node", "slurm_node"] + hostnames: [] + groups: [] + + - name: "local_data" + fs_spec: "/dev/sdc" + fs_file: "/opt/data" + fs_vfstype: "ext4" + fs_mntops: "defaults,nofail" + fs_freq: "0" + fs_passno: "2" + roles: [] + hostnames: [] + groups: ["grp1"] + +swap: + - name: "compute_swap" + filename: "/swapfile" + size: "4G" + maxsize: "8G" + roles: ["slurm_node"] + hostnames: [] + groups: [] + +mount_default_fields: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] +``` + +#### PXE Mapping Data + +```csv +HOSTNAME,MAC,IP,SERVICE_TAG,GROUP_NAME,NODE_ROLE +manager01,aa:bb:cc:dd:ee:01,192.168.1.10,SVC001,grp0,slurm_control_node +compute01,aa:bb:cc:dd:ee:02,192.168.1.11,SVC002,grp1,slurm_node +compute02,aa:bb:cc:dd:ee:03,192.168.1.12,SVC003,grp1,slurm_node +compute03,aa:bb:cc:dd:ee:04,192.168.1.13,SVC004,grp2,slurm_node +``` + +#### Ansible Facts Generated + +```yaml +# pxe_role_map +pxe_role_map: + slurm_control_node: ["manager01"] + slurm_node: ["compute01", "compute02", "compute03"] + +# pxe_group_map +pxe_group_map: + grp0: ["manager01"] + grp1: ["compute01", "compute02"] + grp2: ["compute03"] + +# mount_targets +mount_targets: + nfs_slurm_home: ["manager01", "compute01", "compute02", "compute03"] + local_data: ["compute01", "compute02"] + +# swap_targets +swap_targets: + compute_swap: ["compute01", "compute02", "compute03"] + +# hostname_mounts +hostname_mounts: + manager01: + mounts: + - name: "nfs_slurm_home" + fs_spec: "172.16.107.168:/mnt/share/omnia" + fs_file: "/home" + fs_vfstype: "nfs" + fs_mntops: "defaults,nofail,_netdev" + fs_freq: "0" + fs_passno: "0" + swap: [] + + compute01: + mounts: + - name: "nfs_slurm_home" + fs_spec: "172.16.107.168:/mnt/share/omnia" + fs_file: "/home" + fs_vfstype: "nfs" + fs_mntops: "defaults,nofail,_netdev" + fs_freq: "0" + fs_passno: "0" + - name: "local_data" + fs_spec: "/dev/sdc" + fs_file: "/opt/data" + fs_vfstype: "ext4" + fs_mntops: "defaults,nofail" + fs_freq: "0" + fs_passno: "2" + swap: + - name: "compute_swap" + filename: "/swapfile" + size: "4G" + maxsize: "8G" + + compute02: + mounts: + - name: "nfs_slurm_home" + fs_spec: "172.16.107.168:/mnt/share/omnia" + fs_file: "/home" + fs_vfstype: "nfs" + fs_mntops: "defaults,nofail,_netdev" + fs_freq: "0" + fs_passno: "0" + - name: "local_data" + fs_spec: "/dev/sdc" + fs_file: "/opt/data" + fs_vfstype: "ext4" + fs_mntops: "defaults,nofail" + fs_freq: "0" + fs_passno: "2" + swap: + - name: "compute_swap" + filename: "/swapfile" + size: "4G" + maxsize: "8G" + + compute03: + mounts: + - name: "nfs_slurm_home" + fs_spec: "172.16.107.168:/mnt/share/omnia" + fs_file: "/home" + fs_vfstype: "nfs" + fs_mntops: "defaults,nofail,_netdev" + fs_freq: "0" + fs_passno: "0" + swap: + - name: "compute_swap" + filename: "/swapfile" + size: "4G" + maxsize: "8G" +``` + +#### Generated Cloud-Init for compute01 + +```yaml +#cloud-config +# Generated by Omnia storage_mounts role +# Hostname: compute01 +# Generated at: 2026-03-12T13:28:00Z + +mounts: + - ["172.16.107.168:/mnt/share/omnia", "/home", "nfs", "defaults,nofail,_netdev", "0", "0"] + - ["/dev/sdc", "/opt/data", "ext4", "defaults,nofail", "0", "2"] + +mount_default_fields: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] + +swap: + filename: /swapfile + size: 4G + maxsize: 8G +``` + +## Key Design Decisions + +### 1. Ansible-Native Implementation +- **No Python modules**: All logic implemented using Ansible tasks, Jinja2 templates, and filters +- **Controller independence**: No direct access to NFS/storage required from controller +- **delegate_to**: All storage operations execute on target nodes + +### 2. PXE Mapping as Source of Truth +- All hostname, role, and group data sourced from `pxe_mapping_file.csv` +- Parsed once at playbook start using `community.general.read_csv` +- Built into efficient lookup dictionaries using `set_fact` + +### 3. Two Deployment Modes +- **Cloud-Init Mode**: Generate cloud-init configuration files +- **Direct Mode**: Use `ansible.posix.mount` module directly +- Configurable via `apply_cloud_init` variable + +### 4. Idempotency +- Cloud-init configurations are idempotent by design +- `ansible.posix.mount` module ensures idempotent mount operations +- Swap file creation uses `creates` parameter to avoid recreation + +### 5. Flexibility +- Support for multiple mounts per host +- Support for multiple swap files per host +- Union logic for roles, hostnames, and groups + +## Implementation Considerations + +### 1. Performance Optimization + +```yaml +# Use async for parallel execution on multiple hosts +- name: Apply mounts on all hosts in parallel + ansible.posix.mount: + path: "{{ mount_item.fs_file }}" + src: "{{ mount_item.fs_spec }}" + fstype: "{{ mount_item.fs_vfstype }}" + state: mounted + delegate_to: "{{ hostname }}" + async: 300 + poll: 0 + register: mount_async + loop: "{{ all_hostnames }}" + loop_control: + loop_var: hostname + +- name: Wait for mount operations to complete + async_status: + jid: "{{ item.ansible_job_id }}" + register: mount_result + until: mount_result.finished + retries: 30 + delay: 10 + loop: "{{ mount_async.results }}" +``` + +### 2. Error Handling + +```yaml +- name: Validate mount configuration + assert: + that: + - item.name is defined + - item.fs_spec is defined + - item.fs_file is defined + fail_msg: "Mount configuration missing required fields" + loop: "{{ mounts }}" + loop_control: + label: "{{ item.name | default('unnamed') }}" + +- name: Check if NFS server is reachable (from target nodes) + wait_for: + host: "{{ item.fs_spec.split(':')[0] }}" + port: 2049 + timeout: 10 + delegate_to: "{{ hostname }}" + when: item.fs_vfstype == 'nfs' + ignore_errors: true + register: nfs_check +``` + +### 3. Validation + +```yaml +- name: Validate unique mount names + assert: + that: + - mounts | map(attribute='name') | list | length == mounts | map(attribute='name') | unique | list | length + fail_msg: "Duplicate mount names found" + +- name: Validate hostnames exist in PXE mapping + assert: + that: + - item in all_hostnames + fail_msg: "Hostname {{ item }} not found in pxe_mapping.csv" + loop: "{{ mounts | map(attribute='hostnames') | flatten | unique }}" + when: item | length > 0 +``` + +### 4. Logging and Debugging + +```yaml +- name: Log mount resolution + debug: + msg: | + Mount: {{ item.key }} + Targets: {{ item.value | join(', ') }} + loop: "{{ mount_targets | dict2items }}" + loop_control: + label: "{{ item.key }}" + +- name: Create mount application log + copy: + content: | + Hostname: {{ inventory_hostname }} + Mounts Applied: {{ hostname_mounts[inventory_hostname].mounts | map(attribute='name') | join(', ') }} + Swap Applied: {{ hostname_mounts[inventory_hostname].swap | map(attribute='name') | join(', ') }} + Timestamp: {{ ansible_date_time.iso8601 }} + dest: /var/log/omnia_mounts.log + delegate_to: "{{ item }}" + loop: "{{ all_hostnames }}" +``` + +## Integration with Existing Roles + +### Option 1: Extend nfs_client Role + +```yaml +# roles/nfs_client/tasks/main.yml +--- +- name: Include cloud-init mounts configuration + include_tasks: cloud_init_mounts.yml + when: use_cloud_init_mounts | default(false) + +- name: Traditional NFS client setup + include_tasks: traditional_nfs.yml + when: not (use_cloud_init_mounts | default(false)) +``` + +### Option 2: Create New storage_mounts Role + +```yaml +# Create dedicated role for mount management +# Can be called from existing playbooks +- name: Configure storage mounts + include_role: + name: storage_mounts + vars: + storage_config_file: "{{ omnia_input_path }}/storage_config.yml" + pxe_mapping_file: "{{ omnia_input_path }}/pxe_mapping_file.csv" +``` + +## Security Considerations + +1. **Credential Management**: + - CIFS credentials stored in `/root/.smbcreds` with 0600 permissions + - Use Ansible Vault for sensitive mount options + +2. **Mount Options**: + - Always use `nofail` to prevent boot failures + - Use `_netdev` for network filesystems + - Use `nosuid,nodev,noexec` where appropriate + +3. **Validation**: + - Validate all paths to prevent directory traversal + - Sanitize user inputs from storage_config.yml + - Check filesystem types against allowed list + +4. **Permissions**: + - Swap files created with 0600 permissions + - Mount points created with appropriate ownership + +## Testing Strategy + +```yaml +# Test playbook +--- +- name: Test storage mounts configuration + hosts: localhost + tasks: + - name: Validate configuration syntax + include_role: + name: storage_mounts + tasks_from: validate + + - name: Dry run - show what would be mounted + include_role: + name: storage_mounts + vars: + check_mode: true + + - name: Apply to single test host + include_role: + name: storage_mounts + vars: + limit_hosts: ["compute01"] +``` + +## Conclusion + +This Ansible-based design provides a flexible, maintainable approach to managing storage mounts and swap configurations without requiring Python modules or controller access to storage systems. By leveraging Ansible's native capabilities (tasks, templates, filters, and delegation), the system can efficiently manage mounts across heterogeneous clusters while maintaining idempotency and providing clear audit trails. + +The design supports both cloud-init and direct mount management approaches, allowing deployment flexibility based on environment requirements. diff --git a/docs/cloud_init_mounts_python_hld.md b/docs/cloud_init_mounts_python_hld.md new file mode 100644 index 0000000000..40b4025993 --- /dev/null +++ b/docs/cloud_init_mounts_python_hld.md @@ -0,0 +1,509 @@ +# High-Level Design: Cloud-Init Mounts Configuration + +## Overview + +This document describes the design for a flexible, cloud-init compatible mount and swap configuration system that allows administrators to define storage mounts and swap files with fine-grained control over which nodes receive each configuration. + +## Design Goals + +1. **Flexibility**: Support multiple mount and swap configurations with different targets +2. **Cloud-Init Compatibility**: Generate configurations compatible with cloud-init's mounts module +3. **Granular Control**: Allow targeting specific nodes by roles, hostnames, or groups +4. **Simplicity**: Provide an intuitive YAML-based configuration interface +5. **Uniqueness**: Ensure each mount/swap configuration is uniquely identifiable + +## Architecture + +### 1. Configuration Input Structure + +The system accepts two main configuration lists in `storage_config.yml`: + +#### 1.1 Mounts Configuration + +```yaml +mounts: + - name: "nfs_slurm_home" # Unique identifier + fs_spec: "192.168.1.100:/export" # Device/source + fs_file: "/home" # Mount point + fs_vfstype: "nfs" # Filesystem type + fs_mntops: "defaults,nofail,_netdev" # Mount options + fs_freq: "0" # Dump frequency + fs_passno: "0" # Fsck pass number + roles: ["slurm_control_node", "slurm_node"] # Target roles + hostnames: ["node01", "node02"] # Target specific hosts + groups: ["grp1", "grp2"] # Target node groups +``` + +#### 1.2 Swap Configuration + +```yaml +swap: + - name: "compute_swap" # Unique identifier + filename: "/swapfile" # Swap file path + size: "4G" # Swap size + maxsize: "8G" # Maximum size (for auto) + roles: ["slurm_node"] # Target roles + hostnames: [] # Target specific hosts + groups: ["grp1"] # Target node groups +``` + +### 2. Target Resolution Process + +The system resolves mount/swap targets through a multi-stage process: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Stage 1: Parse Configuration │ +│ - Read mounts[] and swap[] from storage_config.yml │ +│ - Read pxe_mapping_file.csv for hostname mappings │ +│ - Validate required fields (name, fs_spec, fs_file, etc.) │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Stage 2: Resolve Targeting Criteria │ +│ For each mount/swap entry: │ +│ - Extract roles[], hostnames[], groups[] │ +│ - If all empty → target = ALL nodes from pxe_mapping.csv │ +│ - If any specified → resolve to hostname list │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Stage 3: Convert to Unique Hostname List │ +│ - roles[] → query pxe_mapping.csv for role-matched hosts │ +│ - hostnames[] → validate against pxe_mapping.csv │ +│ - groups[] → query pxe_mapping.csv for group members │ +│ - Combine all sources into unique hostname set │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Stage 4: Build Hostname-to-Mounts Mapping │ +│ For each unique hostname: │ +│ hostname_mounts[hostname] = [mount_name1, mount_name2] │ +│ hostname_swaps[hostname] = [swap_name1, swap_name2] │ +└─────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Stage 5: Generate Cloud-Init Configuration │ +│ For each hostname: │ +│ - Retrieve mount configurations by mount_name │ +│ - Retrieve swap configurations by swap_name │ +│ - Generate cloud-init YAML for that specific host │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 3. Hostname Resolution Algorithm + +#### 3.1 PXE Mapping File Structure + +The `pxe_mapping_file.csv` is the source of truth for all hostname mappings. It contains: +- **HOSTNAME**: Unique hostname for each node +- **MAC**: MAC address of the node +- **IP**: IP address of the node +- **SERVICE_TAG**: Service tag of the node +- **ADMIN_MAC**: Admin network MAC address +- **ADMIN_IP**: Admin network IP address +- **BMC_IP**: BMC/iDRAC IP address +- **GROUP_NAME**: Group identifier (e.g., grp0, grp1, grp2) +- **NODE_ROLE**: Role assignment (e.g., slurm_control_node, slurm_node, kube_node) + +#### 3.2 Resolution Logic + +For each mount/swap configuration entry: + +```python +def resolve_hostnames(mount_config, pxe_mapping): + """ + Resolve roles, hostnames, and groups to a unique list of hostnames. + All hostname data is sourced from pxe_mapping_file.csv. + + Args: + mount_config: Mount/swap configuration dict + pxe_mapping: Parsed pxe_mapping_file.csv data (DataFrame) + + Returns: + Set of unique hostnames + """ + target_hostnames = set() + + # If all targeting fields are empty, return ALL nodes from pxe_mapping + if not (mount_config.get('roles') or + mount_config.get('hostnames') or + mount_config.get('groups')): + return set(pxe_mapping['HOSTNAME'].tolist()) + + # Resolve roles to hostnames from pxe_mapping + for role in mount_config.get('roles', []): + matching_hosts = pxe_mapping[ + pxe_mapping['NODE_ROLE'] == role + ]['HOSTNAME'].tolist() + target_hostnames.update(matching_hosts) + + # Validate and add explicit hostnames from pxe_mapping + for hostname in mount_config.get('hostnames', []): + if hostname in pxe_mapping['HOSTNAME'].values: + target_hostnames.add(hostname) + else: + log_warning(f"Hostname '{hostname}' not found in pxe_mapping.csv") + + # Resolve groups to hostnames from pxe_mapping + for group in mount_config.get('groups', []): + matching_hosts = pxe_mapping[ + pxe_mapping['GROUP_NAME'] == group + ]['HOSTNAME'].tolist() + target_hostnames.update(matching_hosts) + + return target_hostnames +``` + +#### 3.3 Example Resolution + +**PXE Mapping File (pxe_mapping_file.csv):** +```csv +HOSTNAME,MAC,IP,SERVICE_TAG,GROUP_NAME,NODE_ROLE +manager01,aa:bb:cc:dd:ee:01,192.168.1.10,SVC001,grp0,slurm_control_node +compute01,aa:bb:cc:dd:ee:02,192.168.1.11,SVC002,grp1,slurm_node +compute02,aa:bb:cc:dd:ee:03,192.168.1.12,SVC003,grp1,slurm_node +compute03,aa:bb:cc:dd:ee:04,192.168.1.13,SVC004,grp2,slurm_node +node01,aa:bb:cc:dd:ee:05,192.168.1.14,SVC005,grp1,kube_node +node02,aa:bb:cc:dd:ee:06,192.168.1.15,SVC006,grp1,kube_node +``` + +**Configuration:** +```yaml +mounts: + - name: "nfs_home" + fs_spec: "192.168.1.100:/home" + fs_file: "/home" + roles: ["slurm_node"] + hostnames: ["manager01"] + groups: ["grp1"] +``` + +**Resolution Steps:** + +1. **Roles Resolution**: `["slurm_node"]` → Query pxe_mapping.csv where NODE_ROLE='slurm_node' → `["compute01", "compute02", "compute03"]` +2. **Hostnames**: `["manager01"]` → Validate in pxe_mapping.csv → `["manager01"]` +3. **Groups Resolution**: `["grp1"]` → Query pxe_mapping.csv where GROUP_NAME='grp1' → `["compute01", "compute02", "node01", "node02"]` +4. **Unique Set**: `{"compute01", "compute02", "compute03", "manager01", "node01", "node02"}` + +### 4. Hostname-to-Mounts Mapping + +After resolving all mount and swap configurations, the system builds a reverse mapping: + +```python +hostname_to_mounts = { + "compute01": ["nfs_home", "data_mount", "compute_swap"], + "compute02": ["nfs_home", "data_mount", "compute_swap"], + "manager01": ["nfs_home", "nfs_slurm"], + "node01": ["nfs_home", "ephemeral_mount"], + # ... etc +} +``` + +**Data Structure:** +```python +{ + "hostname": { + "mounts": [ + { + "name": "mount_name", + "fs_spec": "...", + "fs_file": "...", + "fs_vfstype": "...", + "fs_mntops": "...", + "fs_freq": "...", + "fs_passno": "..." + } + ], + "swap": [ + { + "name": "swap_name", + "filename": "...", + "size": "...", + "maxsize": "..." + } + ] + } +} +``` + +### 5. Cloud-Init Configuration Generation + +For each hostname, generate a cloud-init compatible configuration: + +#### 5.1 Cloud-Init Format + +```yaml +#cloud-config +mounts: + - ["192.168.1.100:/home", "/home", "nfs", "defaults,nofail,_netdev", "0", "0"] + - ["/dev/sdc", "/opt/data", "ext4", "defaults,nofail", "0", "2"] + +mount_default_fields: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] + +swap: + filename: /swapfile + size: 4G + maxsize: 8G +``` + +#### 5.2 Generation Process + +```python +def generate_cloud_init_for_host(hostname, hostname_config): + """ + Generate cloud-init configuration for a specific host. + + Args: + hostname: Target hostname + hostname_config: Dict containing mounts and swap configs + + Returns: + Cloud-init YAML string + """ + cloud_init = { + 'mounts': [], + 'mount_default_fields': mount_default_fields, + 'swap': {} + } + + # Convert mounts to cloud-init format + for mount in hostname_config['mounts']: + cloud_init['mounts'].append([ + mount['fs_spec'], + mount['fs_file'], + mount.get('fs_vfstype', 'auto'), + mount.get('fs_mntops', 'defaults,nofail'), + mount.get('fs_freq', '0'), + mount.get('fs_passno', '2') + ]) + + # Add swap configuration (use first swap if multiple) + if hostname_config['swap']: + swap = hostname_config['swap'][0] + cloud_init['swap'] = { + 'filename': swap['filename'], + 'size': swap['size'], + 'maxsize': swap.get('maxsize', '') + } + + return yaml.dump(cloud_init) +``` + +### 6. Deployment Flow + +``` +┌──────────────────┐ +│ Administrator │ +│ edits │ +│ storage_config │ +└────────┬─────────┘ + │ + ↓ +┌──────────────────────────────────────────┐ +│ Ansible Playbook Execution │ +│ 1. Read storage_config.yml │ +│ 2. Read inventory (roles) │ +│ 3. Read pxe_mapping_file.csv (groups) │ +└────────┬─────────────────────────────────┘ + │ + ↓ +┌──────────────────────────────────────────┐ +│ Resolution Engine │ +│ - Resolve all mounts/swaps to hostnames │ +│ - Build hostname_to_mounts mapping │ +└────────┬─────────────────────────────────┘ + │ + ↓ +┌──────────────────────────────────────────┐ +│ Cloud-Init Generator │ +│ For each hostname: │ +│ - Generate cloud-init YAML │ +│ - Write to /var/lib/cloud-init/... │ +└────────┬─────────────────────────────────┘ + │ + ↓ +┌──────────────────────────────────────────┐ +│ Node Provisioning │ +│ - Cloud-init reads configuration │ +│ - Mounts filesystems │ +│ - Creates swap files │ +└──────────────────────────────────────────┘ +``` + +## Data Flow Example + +### Input Configuration + +```yaml +mounts: + - name: "nfs_slurm_home" + fs_spec: "172.16.107.168:/mnt/share/omnia" + fs_file: "/home" + fs_vfstype: "nfs" + fs_mntops: "defaults,nofail,_netdev" + fs_freq: "0" + fs_passno: "0" + roles: ["slurm_control_node", "slurm_node"] + hostnames: [] + groups: [] + + - name: "local_data" + fs_spec: "/dev/sdc" + fs_file: "/opt/data" + fs_vfstype: "ext4" + fs_mntops: "defaults,nofail" + fs_freq: "0" + fs_passno: "2" + roles: [] + hostnames: [] + groups: ["grp1"] + +swap: + - name: "compute_swap" + filename: "/swapfile" + size: "4G" + maxsize: "8G" + roles: ["slurm_node"] + hostnames: [] + groups: [] +``` + +### Resolution Results + +**PXE Mapping Data (pxe_mapping_file.csv):** + +| HOSTNAME | NODE_ROLE | GROUP_NAME | +|----------|-----------|------------| +| manager01 | slurm_control_node | grp0 | +| compute01 | slurm_node | grp1 | +| compute02 | slurm_node | grp1 | +| compute03 | slurm_node | grp2 | + +**Resolution from pxe_mapping.csv:** +- Role `slurm_control_node`: `["manager01"]` +- Role `slurm_node`: `["compute01", "compute02", "compute03"]` +- Group `grp1`: `["compute01", "compute02"]` + +**Resolved Hostnames per Mount:** + +| Mount Name | Resolved Hostnames | +|------------|-------------------| +| nfs_slurm_home | manager01, compute01, compute02, compute03 | +| local_data | compute01, compute02 | + +**Resolved Hostnames per Swap:** + +| Swap Name | Resolved Hostnames | +|-----------|-------------------| +| compute_swap | compute01, compute02, compute03 | + +### Hostname-to-Mounts Mapping + +```python +{ + "manager01": { + "mounts": ["nfs_slurm_home"], + "swap": [] + }, + "compute01": { + "mounts": ["nfs_slurm_home", "local_data"], + "swap": ["compute_swap"] + }, + "compute02": { + "mounts": ["nfs_slurm_home", "local_data"], + "swap": ["compute_swap"] + }, + "compute03": { + "mounts": ["nfs_slurm_home"], + "swap": ["compute_swap"] + } +} +``` + +### Generated Cloud-Init for compute01 + +```yaml +#cloud-config +mounts: + - ["172.16.107.168:/mnt/share/omnia", "/home", "nfs", "defaults,nofail,_netdev", "0", "0"] + - ["/dev/sdc", "/opt/data", "ext4", "defaults,nofail", "0", "2"] + +mount_default_fields: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] + +swap: + filename: /swapfile + size: 4G + maxsize: 8G +``` + +## Key Design Decisions + +### 1. Unique Mount Names +- Each mount/swap must have a unique `name` field +- Enables tracking, debugging, and idempotent operations +- Allows referencing specific configurations in logs and errors + +### 2. List-Based Targeting +- Support multiple targeting methods: roles, hostnames, groups +- Union of all targeting criteria (OR logic) +- Empty targeting = apply to ALL nodes + +### 3. Hostname-Centric Processing +- Convert all targeting to unique hostname lists early +- Build hostname-to-mounts mapping for efficient lookup +- Generate per-host cloud-init configurations + +### 4. Cloud-Init Compatibility +- Follow cloud-init mounts module specification exactly +- Support all /etc/fstab fields +- Provide sensible defaults via `mount_default_fields` + +### 5. Separation of Concerns +- Configuration input (YAML) +- Resolution logic (roles/groups → hostnames) +- Cloud-init generation (per-host YAML) +- Deployment (Ansible/cloud-init) + +## Implementation Considerations + +### 1. Validation +- Validate unique mount/swap names +- Validate required fields (name, fs_spec, fs_file) +- Validate targeting criteria reference valid roles/groups +- Validate filesystem types are supported + +### 2. Error Handling +- Invalid role names → warning and skip +- Invalid group names → warning and skip +- Invalid hostnames → warning and skip +- No resolved hostnames → error + +### 3. Idempotency +- Use mount names for tracking applied configurations +- Support updates to existing mounts +- Support removal of mounts (empty fs_file) + +### 4. Performance +- Cache hostname resolution results +- Batch cloud-init generation +- Parallel deployment where possible + +## Security Considerations + +1. **Credential Management**: For CIFS/SMB mounts, credentials should be stored securely (e.g., `/root/.smbcreds` with 0600 permissions) +2. **Mount Options**: Use `nofail` to prevent boot failures +3. **Network Mounts**: Use `_netdev` to ensure network is available before mounting +4. **Validation**: Validate all user inputs to prevent injection attacks + +## Future Enhancements + +1. **Conditional Mounting**: Support conditional mounts based on hardware detection +2. **Mount Dependencies**: Support mount ordering/dependencies +3. **Dynamic Updates**: Support runtime mount updates without reboot +4. **Monitoring Integration**: Integration with monitoring systems for mount health +5. **Backup/Restore**: Configuration backup and restore capabilities + +## Conclusion + +This design provides a flexible, scalable approach to managing storage mounts and swap configurations across heterogeneous clusters. By converting roles, hostnames, and groups to unique hostname lists and building hostname-to-mounts mappings, the system can efficiently generate cloud-init configurations tailored to each node's requirements. diff --git a/docs/logos/Liqid.png b/docs/logos/Liqid.png deleted file mode 100644 index a0c91c23f118df95aae6093445de789b8c20695b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27613 zcmeEuWk6L++vuXZLqIwurI9X??rx+@x+NtAq`SLC8YCp7k!}!4NogcR;O>oa^mxww z-tX7_ao^dq*PivvQ}fi!4!D`QSpv|d9!Wj|ARqz281w*cJ^>gHosF#A0SEvZ002G^ zXb+$kHnuf10gT%~5C|v$2O0_%5d{D!(E)%{9{^Nx0KjVgW*!ItV4wk z00#qy;OE4DI=JZoFyJ8?pl6{VFaSsl2q+AQn@->ifPerXp+J|P4GtC_3K|9y;@iYe zFlYb`0|5mK30fOyEwE4!00cA?cp3r{3K~F%fhEPjWMjs{X6L{aQn16RFd(O(%pzmK z07-y?K|p~AV4xwv_#l%)L6c&TvDl+CD?YAl$8=y55{}LuT_$IZsluXQSMU>2${BOS zHhdZ_KVJLSFo!bT|N}5FvyC!nFEkP`a@zsVSx0B^98TszwN&k`2W`eQ1=pL z<7EoI7yv9YQw`R4m%ygL6@3o`XSjW{%Kczco;5j(ke+GS|Izcetp6A5w6XGa4 z61v5Os42Y1Q}D_c07&xhm8OGU@%rxYf$$)+)O;!PngIv}dN3wbczTRu325+pBaukBI0h$GN(iM{|Gj5!z)VilcY06h4unPQncHWAwyOkNS+g*}JZ>wQ_M zb%b0&@F5X1_||YU%2dEqb>BB4*s-*%?UXm5rHm;E!C{0Fa}SxoRRIrVy;w#7pa>@m zf%g8bV7LVLHmJ>$$cF$L{B*4EZz4fAuBVU6bnN*unlH3WrM52R6NDh2n++HrZJm2w z#wxp93Epi3Vg0jcd_NBWkA;};0BxLuVZ#ZkonzwSx5YvV$pQfJp$3}}1U(fgh|jN$ zB%fzK@{Cd)bXr%!iUwm7m}>mKz{sUo{D=FPo&2_pCx6YGapK*5t0E&)(v zoY-r?vn03Qyow14GBrtlKpx;UX!^kSSHU0$#0?Z=YaXql_-H^>(MDhSjv~*JPz%f0-C{DNrIrE$^wmk zX)xsDvArKvy&p3df8vwHr67-0m8@O+OW@CFSWzfHAlL(~@}I2_#ml&a2Z0UZ%+F@h z#+R$*3*bQ3Ku}RF0f>HDKoph`h*E&j7i)##7wMoO)HjLUw5Z!Y+T#&ET02~P; z^VM}DyQ=|+6uFJ#r>IYl5lqC*)-2Kh040!B0mX_h>KmIUwEd{Xr63_J#z0aGD3PJ! z<`htq%Sdm<#>5-|FhVZ@34|<=7ZUC)A>Rx9T|AuNOXt)Giu!7KWxXv9LEH8XgREJH z3zdFrORAdn>QchwLz&vOr}GqkeVIg=9>IwP#wD3G7smhQi2V2i1W9_34A}8ZgJllu zKZ5~aWqaJl0tx@S9@t1sM)b*2)RVfVyaW@Vz%M?1?*o@!rDZe!a>H(@jQd@;Gl+!Q z@;Va5-xC0=X=8k?G}nA@xj)DWOUG5Rvh$z$ zZ14wzxs&Cx4~#Trwi|DNw_Bkvd+u!81b1z`Pf4xYa=Q(Me-2B?1obv>SqFf-LN0!t zmLq?b1i%=!3owL6|H~o}CtaY8gj6u<~n(5?2MwR2Um>0825SZZ_J&UC#xj zHZbYGSV5e@0P{!+H1Cxv2Qk6n`%mF8_5~skJqyg_dn24Z)O@|do67E1`HAW?fAA_P zny9f>%P`KD&XUa9V-9J2oqjEtL}un9B3LY_>~B>6Tpfp{&7lv%K^Vk-)k4INL3;?e zCjDL6X>eM$juj_ssKQ& zZ^who?9H;N^pcw)NL7Cwb1q__XPock3jBZ|W`CX^c>zQDYXN@aAHTR5bn85H;m-$?%XbOqam z4?uTD`vPz|){hr}kR-f6^A0jx6L^4P%(L{XY<%N>nd^aMES`*}wn$TeC{nAGi%;I_ zLq@`DOiP>jFygjv*#GQjZ1NF+va{_1AntSIUqX8@^)}$oP;-o1Df0G}lbHEcE^uX- zZ`*x&cj=_I7j(#ZuW<@w&pOOL?6+qv-f6}=HTZ@MPBDbTDez5)j0*7cGzmBA&6)+V zipR1>xUOK@Lka{YN&IGQC|s5$h<*=K1s7j9p=q|PZ|uHb;R@@hv{pmE46)4AWjso5 z1MRu}`QxnJvuD+V_N@PXpTt_SeupY(#E&;iN5zZH8h5UNZV*{xU3Zw-9& zvmvke@Fovu0RYbjPSB~syFMK>1~+i>-VSb0N^{C_M9S)Ipp$E#G1~};SoC>P|CXt%SZ$-WjL39U;(igZ8mm z(Ek0B6a>Jhm0*Kvgg7@^N0f^w0s&}ER3KGk0RX!Sd=R(KP<(;M9Y*4NDigPzqyd8b zCkCR0Q#BEDld94*ebW zO$LZ7@hg!?^=$Fdk*uk7^vBw?zHd`mbJDntcZ=WjXS~yWKr58rY3LWGV9Kat|D+Xx zr#|jn@`>c4RXcz|z@OtR471<1T5HlD1X4+ST za3f3mdDVRsCk(rIk43dKZY`JIgR8FQa`m!|!oI$~ zE0t$vo;Kj?(=&~vX*%ofNXJ^w2lwgBuHRI}^c2`al0qnYFr`UlN~^y2Sf-dpAa0N_ zc|>9IQPXIG>}}mw%Iy!z%T%>AUgf-x6J;gS2eonU!G}c9G3shr<;9T~4{e!@t5$;6 z^=)-6m*p^(cKDuXcQM9Qu~;p=R52nsG}ZNuF|FV4-4VkiPv99KPWQ&CU#3wz%0yr+ z=H0dDe5mfAzciX-m0F9knr?{`Y%S7uYr^C9QPTyva-^Sii}eWpcFTT_!oZlEsfCLr~Rs zmZN%PG;tsGeRLIKiL**u-)Kq$S${TpV~^$o&c%Rq?S4iCi zRBg>BfXD99CPVR*nhGah%}UK>RCp{|9Iux2L*>n6@rV69)C#FCTj3?h?cTm3-{N^FsGBtOMbddS4@uPFs$>F8kr-Mv+QKJDl)x-snqkNX_G||A`XH0i^cBBG& zB;9bukmLKw$&ffbYX z;afRV6zz@@49XAOyz%E#e>V)5n=G&9Kd$7WjT}nO4K?+EKJs-*e_F5Z0|0KCM37{iU#l_U;vW+8m_9%C419(UZ8icJG0&taXP0Pn~ZJO^lI)BtCpdH%G=y`a` zLsOB?aTHWgLH{tP`O6b+qK?hHhf2Zwh?B3wjqWbZ|5D#^%F09E?*(wz=%Lg^a@O)b zm(#uToAR?LM^dgGt$SbS*!Dc|7MtnS)>$Gf6lYF0g+c(84`GEQh`-j`r2FG7DSbG` zI`zD+gh*6TOU=`xa=$6fuEvkU>8c&!Y_yru!0SA!5=KQR$FAMsaal$F6pwT4b7*p0rf_%p)8+-K=hPZgA+ z#h=KEIKvfJnU)l)>Zl2P+JEsLeD&VJyjNDadFs0kz#x3!K$oo_^Eslm^(Z(vEGDd1 zWGg5@q##yODt)u9ex9%xkebS0wdX}v<{_$a5>&Z`Vl+n3lgAM$VMQD@#s34WpIa!S zep?MzAuwOGYBGwI!%cWNZrU5;!uA(;sELl^x_GhR$+C}ABc;?%Kj`^c*(!yk3JQPW zhudcaKc`~6+@v-*;CDQsS?6)69`Pn2<6uw+yNkszSAxEl-|lcHI8uj?Ba1XSU)!rs zd}ZDnD-yK%ecSH`SA!z%eNOmH`5=+17YPpXM`d2+cJMd0a?ZDuE=UP2vJ`<2o3q@N zK8d?C8^nba4XwlEJat~T7_j=`YU;MiFV)*sJDZ;=o9WQlYE?_sIx<}b{!shSG7$+D zLb?PRLQraCf@?44+}cUS=TW)DB!MlRurQAYT^!;;6U#m=VZQ#(N) zbDf_Um0A&LYi!T;s7>+IRZiAMye#_@7&iKFXaiF&<){&i5-gYM^q$ALUC9V}pTMDF zVTTJ3x=q1ZKaVqbI;S8QHH}+6(1b3<^7D7*Evm9&_L@=!DJ}mZi0zazu$+!nVg0kY zhL^*n?ot6ig-NKtFGUZdR_v9!&@o#RAI-`M8XKsbc;Em1mg!l4)~hPD)meJ`Pw@?o zTE&gxHvrQOaC-brCbdq_rMu#0+JH*Vly6 zro{yiX@E(a!fS6l@POhj141G#kxXt1%OKu*xZ@*u>otQRXh9^I*RrB!>;q}x+(HqU zsk9~f9kEP~D&xSjASH#_Rf%zK2a@(S((E|UeE1kcJf{X?(xa#sWCS9t$pcB=sL`g` z)hQ-JA|Prb5@ff%exFPOiC)_WRA&^agMp^gqs`UJlrEDKGR>bBNTe>8O5>m z<9$SSc!a2vM-d0V!5&71k^p&!7QQ`Lusy{3t#5IG$Of4|s-r};%A$YNEGh>Yc$&^4 z?)AI%frGdgY(b*W$$QraAWizsIDG=^SE%W``_rEmIF_2ID3uE%3%z@(kda zMNFt<8+jHJ*ezW99@v@(0OYx zR0WE2Ka{j^{I+F@YK7Tra#0hu!ZZh?Si&SaWtGL}c{V_V0!8LZpn!dv8Gg5YnkBr# z#SIYh-N8`b9jvrh)1&0JEl(u5TFmE66Onf?+ZJ+7UDb&ApaVl)(+5*m*ECu)%_y7Q zi8>-+B*#!VXnT`u}@jVLik zG#QoXA|vbIfyEMtrmaTnNM|fEUSNcvk+y)BNeRgBWv0uhK<}5C95^VHwm@JP zXNi6hbh|u zj*&boPw=<<&I%DfRB{V}Ps@6A;J|roO6SSQ1RKcALFOMhR7N1V`x>=+4uYSXup`4V zDaM)jt&G_uRLgcWb^~PCHE_$YOa|HP;auv%6(t3}7(-wnuG~~F}2qV2*okSUN?CuYjNyk!D9KDT8F z>XrZoQTJOzJb8-|T1T1is`l}~4ka4KQJG@&c^F-Pe=i)dnmF3dr;o%CyOfW`0-*Xi z$=+fcYaB%;5>Gxu51tZ3v%KUMtJXO6?0psnO`;~CgtpS6y8TM^Qu9t}rsai!|IXX8 zpn*(U2%}wn=5Nxl$iSGL!U)7Z6#*pbbD|sm7}g&T$z4_^D`5#GqWlfXmp^;u>6x#T zoT%rt;7r{$yH^oi*na>I7w6eK5{!QOswQ&wSR#}GJqFJ(oT6XAic&Pt#Kh#{e4}5I zzX1}{azK#cyJ;XGK&H6?mJkm!F%$LSaS~<@6J{bX=P^384IfBbiz7S-^ zQYezt>f=N zOCs!b~S={JSD5SFKP^lXlJj7af1Dd zI^x4<{EH8+L#txXc6P;#n*&HK7*(jq0v-yDhjAE)B^@qeZyiA{l++{~>YglPo2z}X zzxz%%WSAVYYd!IfA=~0tt_~B_+|eoNJ7`@T`P%(>;?*~Rg;7p=dV|e%X|>SY{4<&U zS-xzYdLI+0oHuXk`A>1*eqf~#ON%5a~N9Iv@qfXrFr!#Ve&8bCnPxuGy+Q!0G)^WJe#}qOhb+;gNShA6?*xzH0_3 zWIj`LDj_aNyw6N5((iO&-YlFfI$P|Nx&8o(3LcA!9m1~kOKA2?cV~)ZS^oj9XOFXy zJVzI=Mmk+qRXV*#ZouO=f4KHA??7%D^k#RsgEA5Qm_Ky!LeYj#cI9F7ebkfBNH8K2 zqDO{g2gDT>M-|@79_D-+;en0UMO)m0o!nYR!Z-sUPp^q+G>@%(dgP8i7tBz(EI>sv zuP@2>VI5WP0)sj)qoj;GukgBk<{It>*j2MN9eP>XuO5ceAq%K!y!v7oY=kpbd_siC z{`#)XQ@-7L{tTD-jA355544K1A{M1hE2Qq_=iNwajKR&ukGx&ej~hk}8ZQ+!@1yn+ zaa~)se72W3ASAhv7xSUBJ7Qmd?O~WmpMBg+fs!3_I6EfNx%D7Cu$bIfkeIaQbHm53 zneZbr20aUVu>GHiCLEFG#Pcp!x_Q&qM{uJ(o}P4z$#7Aw9m&ItM>d-Esi^$ew7sg% zu{bH5H+CGetxH~OyM&x5v0V_>6XN%guN0&GX<)@!c*yY3@f%kHhCBL}^py2-Vz#&# z9i|3xBQSt2ohkx$%&?*AVMa(>`f|IwJ8ZRdMr@0!13oIrev@A0Ytb8^tf2`b;~`bp zAQL!&P>LKrC>_FynX~1sBCk`bRgU3d)2~LIgk}N~%)8p>CaUBodObnj%=8Ic(Gbgd73IR%B?-nd$Ix15&@{E}ZQuAi`yPVb%TTa{iO zNcqlI;qN(>h|Cou)kI6f5E73#kq+^7-8!*Q)O}OAx?Mn#oC48Q;p`qbwSRRu<2E_o zS41TFV+99^K(7E=xCIt`ulSWR_g4>MU306%;6FC4Fu2Q|4~q?kPjjlIX+ApTcEh@(jvluV z3Vp~{KWcfTiD7XAAYNA8MqI*u_FLO=W)FC1ab@uEJ;X;%JZd4d5a>I^ixn|Uy05v`)-}}z1ppbS`yY5d3qRavSNFy=B=NK=w`{~E8sIl zE0isBVSjc;{JwOR)Ygj1arep2jHelhXer0fxkS)ul!_pHm7u!5N~tndxXz;dc=Fau z3U@@oj75ek`M(I2g;o$-`FlZ%S#gHdL>KjWcLO}xrJNF*_MK32_|r#swC}MJ&qbV7 z2|hL|cvC=myl^n$S?Fx^z~-b*BHuF+`?HryRYrNO?p1!MEb6DX?pJw;C}@mt-Tnyf zhIHWsaHKwUvCAW-FRl1{0btz5(i5Fm{BSBLZ5*oJ=h`8BtY?q>eUUMn>RJ(4j%~t6 z3)3%jrbT=TQuGLAnX?cB+Y6=QCxvX~KOb`yez`~;zw#X1x_)CiLp;r9jb@dcu*sb& zBg@Sd@`rPtPv9Ga>d$mN&+|TK7yR42{$5wf?<#z35wYrhYRNm_a!+3K(c^tv|KQ|O zaWpoy2x8NhgnWZq58P8~?2b9;gz(H5h$t@N1Kul7epc%DXq@Z{-QVTIPLEHrG0(er zbx)4LRYZ((1Pa>IG%=W{i%&C|Hm@q9)H~Z(f7zOUm7MLkc|W~73}$*Rw2z5K!34Yw z>b>8lp_-gA`Fqj%jPQzVw`kwyKnz+%fA=I0+lwa`D{Og|BER;v?20xC>$8BTXa3r z6v5dj@ntq@1oBe+p?ws_4pv`(>E*}XWo+MTOf)Q+vV(<}NT$s~ihWU1<|Xsh6LIU!%1v)HC*HNSU#4ZBwYuRa~S9m9yzCxrLdo{ zOkrL^6n?#)Htm{Ut4qJuppwffMp@~8FyR+w*YWo=iH^_>KysNULJy&_oEmcii;o66 zsDQWAANzf8&;+-w!gASo!{QJtS8f`XCE=Bo-85fc+bT7>SgMjBT zDS%-qpXMm4oBT96+&A_lrE&l`}i1{=^LJV>>5mOCe$RYNZ8_0 z*^^O0^(ZxvCgwA8Wb!uLBsrr7;j5-U%0mAAH7B=TOd^VPj|VYcfmzib`Lb4cyZ6JF zVIVJbl5ZUgQZ+Wp*LX5Rx0n!=KCP{)cxSc9Mhy z&@4y}kmC%YP-T$v2QM>Rls9zWZFfIZ@p};(XQot4y&qD!-uw!^-N8{=j#Dyw@qVlQ z9}dwDz5(X7RID?Sk(}O?cE**w73H%#F5eZSS#bBk%f6;@S4|PE=iS${r^)!X|Nfe% z+;$$v7JO5xG|!6BV2|U1-G}i{3G{L*H9DAC^~mE&qFtVl+rOJE^($3}@)UnRr>jC8 zK)oMY`H^>3b#Y2~i{8Q%&3o}(&HfCsf!I|j!Yxy@qb{S;g!P)5>zr0sO=%xe_alu48*=5xjVtUZZL6?bUrOc&!y`S+x~ zj~L+Q*$`3C@G+(+^UKLltkiP%=_?BCA{jA*z7e~K$j*b$V)->hCmHnSCsXcue9v_y zu#supSU;2v)yCu#n*KfDO@eKr^cW`Mo)5q+o^poRU$3-5FykN~JQLn}v&A<-SU6K1 zl(wrVa^ZD`y`_Dn3_1#w$>fa~pYISG4V8oTg;B|)^pP~TGX}7#{N9GRNR!3JG6EF5 z9`OgTpXrxSw$W;JwdEw66F(R#Yh=tjFCo+K5^;m0 zhvtU5I~B(0dV@VzV^TEbNlrOf;v@S;=262=@iI&okSjKkK-=1XL2zQXM}0qFP=t~^CR`x67$||%ZV#` zip@xELLH?^KZc;*nY_(dBM<*1$4&GJR>xiQ4D|Vj2SMq!O8lSDLkSDY%uqLiq%tVc zQJ^D}>Sl?u=8awapG420)QOvO{>QgySy|L(#13>M=ufJ6h3k5EVV|z&7QG5F zcmtj7O_90F6uq~I3@xIHT0Z6`#wWWbAUddo{CvU`Pugh(GvBjlD}$Q$nRE8FcwEMy zj9#mo9>ad>d$p7fGx!uoPTYBln!DB!BVg9qPPjPCI>-12(xKxTGRs(UrXByY(oQ)< z1(W&3p^_8nnlu*KX~oMO-&>-u5W$ z+}Gu!hu4>~X;fu<7)&SH+uKh<`|cX6hCoV5)Jj~W$TK0e{-go+%+YUl^^-hNHA!k43;oT-wXQZAZSN9(5_<9-ok&;#-a>Cac9jP=UG|@~+!Hg8~!}^gvI?mcj zoL4(opHhyGYc7P#aObQ_`;@)$;5#62Bf}lsS&vK7cB)3Mw(ZEL^^|hW7fAWbJl_;% z9@pFerW5F*5C)9h=%R|ow(70#ZP%(#6kl*B%p5UZqXY3#ceD~*Gn5;?-k*CJUE$xV z{>q;k^)2RBeWpNdlJdkwBR>gJs%`uF{NY92^?f>lr1Ty-8XX$FW3IzpwQ|2RE}l#U zP6tw^muzT95i>*&5M+D{k751x#2glDzxBH=GCHsku+vQ(+32Lkk)yEe>)J-W_2*12 z0A{JdJ!eF*FHh?|c+O1~S4OY+t(aM~-rByH21z_{ulwr#q&4!GR$=S)YXx(Brk<_! zh7hL4<`n0imoEr5j~13gwRP{vQ}ZM&rPzG*$w9VwSJxA5dte)oLUzuNCU94n8LF{e z9+pY!X??5`+kEuZSoDZ^9a=3D-<&+TOkZ!TP1+C+A=1z2Pa` zFeFT}m+_blddavH&dlxa>z~nt@>5%Jx6{((EnscEW=4JbWbRoG4+Z2tC4Z>*M=4$H zW-Y`0BLO^-*>?3ruZ|D4+2P-{0glR2JxioVmNER|_FVL{H?*0@j-Y0iYPFMaVs{qn ztNhhVwSu0vBg<3I(O3}}E0J4R1i4K|K9eUS+3$!#iwv+@!RLP z3dcy6Un8@>dt|i^Gh@2{8N2~IvOjJ=QWf%4D@2jWM>N<+XvrrI2-=SgbX|yy2_=!IBEb z7TIMc!Un}K9)AWTYH&tm%^@fZ_~yL@ITyQ!-Z3%-Q&f4mkrLwNb82UBX2xyeg@NQ*WU4{=YhkMOo{Qc=+E5RceC6AYrSC^ z;OnR6htIPhk(8vN?(6!Oj9;a*RA#xbY^CaQxR<#xdzi3~u7f^+bo;i{8vqql9fC(a zQyY4cpY;65UZ1;yHP^(bvelcL==GEHgF?{wFyV|#tMuC!Wkw9|kX)*>k2&`Ua*i|1 z-eOzd0PuUG)qW5L%*L5Ic+=SuX;pS~rMKzB$ZE#I)9Lf!d$_#KOtoFUzDF`pOR91QdrkVEpyM}dj0#w+-XXQKc^agk}1(n-r(ao>aJcx z?}$0C(fB$MXhDygO6{z^tY8m7I9scK+~9O!3?L;;FQsbYoF{pNH=g_?l6-M_c(DH9 zYnTn+fX!dIF?&^b05;!QlS3Z6RQ>RMj!%p2lNd>zT<)ZMqi2YaZ5l;F+#WcfZF#20 zvyFH5Xr@PHB3I8WbM&Woj-PzxX|fX#B{r!i;av0|tX~B|tgpw<-MBf2vm9AOg%HTd zE$zedNV(J17FYy63h8R&#P^&`8=)vWyl^_wCk*+T-}I%ZR!9pLb$TIXBqXLd$iiA}wgogQdN%&I?ZBeW9*rLZ|eQ$PvTz zC5lhQ^d%bX4G`^YePL~|`}9}~T$>gFHRP!-KxBTmJ`sKnM+;^vo$S-_fa@02H(}?C zmhnd!k;}oV0I-<#hxtYdb1&S;*@(STzIwrmph}J2pJOsWLfU`x znVfksm|T$7=DLJmXcp#CRLRJFiBFNH-r5a27rkH7UPb0ceZJ7o`(Dadx0YN5Xy9bz zX#x?^_JrG*EWJj52|S)n*^E|SJ(MH$#6J0K?bTGmtQB1^GRq1JIotE{+n$?l16M|? zC3F51HHbJubo!%xOZBHax{F*EDoWCPiueHCi&fwql4D6q zq_#C_dVtv%KmVEem%s_j5a_E?3)(1cJM1wCn10=9qdz>#0p0I~s@^?X=pQv7;uY`z z@u_N9*@9Z>B69bqMQfETlChB=)dE9epJPjp)nCVOY2(AW<-`N8K!aS;y$d3&xoSQemNpud}7&|y^(-NS4k-;7Qjhc0{Wq83 z-k=tpM~*ihd7J1<{FZsF533XuAUz<)CaXVoJlqV=?DHPvR$@PAia9n7_2JouqB*=5 z0+Dm^N()l>nXeOa12%#KQjHh~y4L)h_Ll?hA6rSgR7X_!0M7wPNCZzl#Hh^TS7GfJ z!qUz9L1IzJJDhe+VeJZlJKlL=M zm-CPxrw1>ejK&_{<~7Mub$#ayp6>s#8@DLLR%qb0fIou!o z*3D(}kxvgdW%C)%rpdqlTOY-A6}~WM0DVA<{q*(4V!y2=8BZ)*x!DdOTJ>caLb6cz zQL%&}BfQG#8-t({)p^h_aWr=YgZ*QzFIoQWm39jf&vR`FkJ(7nUN~9OVDSc+4*vGz zo$v31=q_rVc5-zIHm4sovJ2D9O0pALXx^E9aAki-Yfs&wDGpMG_iUEYg~tY8m(a+& zms$+kBtW3qA-XsqK&9xC1gV_$P;YL({L6mv_=E0r`O2~C?1)tSQ3Z;&bRIz?8Zn`8 z@;%ZPNa0$ttdIwmLGPMePrEg!j=$|5fd&g5;4bYo$MtK92w742AP z`t%Sy#GVLWMc`dVN)ay3hgTfO66IBFGF8K#QPZVDgD^+HP zze&1BoiE4p$*k{`q}~mk=RmZ>GifjszC%#Il5Itbx}eZsH^93Y{mW9NlG!I2Wb3fQdK z_cNHIOHbu>mmZ}g3mSLdgPx$nK@1d$nyPWR0YD$=ef!bF4|hIU?8va!DJoY{r=yR@ z=?_hJue-dbWR*{4vMSLhM%{Q7qfvG&R)J6&fHOf~q^{5j=cE_>DIA(<5f4rD?5$bV z%z9YD?x_%taFl)2tB)|I~2aab~kfS#F4#)PXStaxLBod{(Q`74zhU{d}{X`Y5~ z@=Bn`So<2UFLmH7q;V`AXAz3cD;!}euBd$JdOKX$h%o<(S!YOvb5U#avUc>$DtB+P zV8>wn9=zt|ow(KluK8DAdMJF53A-SW2&p6|xR~SVIrmTLDGbe|areJ=&T8a4m*dZO zTksta472Nh5@xNG+JRwtiGhUoQTopQ*SFj^fQ*c^VHJ5q{PnT^KF9U3rh|anI)Rla zW#;3f{IIiYqvT4)dhN4kzD*A)WQ-qgEcHtdeB!9vF-mJKpr)#n|5Wj=^)2CB)nYTa zqso_#J*Tf%FLe&D%H6IfZh$WNuPVgB^4I49b@VN|+xzMsO_Lv9!cXM;`!M2gDV#z@a&+2|%@hNP?=tMCsToy>GV&944F&h?@G zJ~mZ`=GaNXTjh_Jow~}bP5#!kS@4XfY>Yng5H18G(6}tUGZ4C?K9xB;TBhnOoWok5 zzD%1vjzeM=wATMjXj(4gC>@8h4y&_8B>LMUYn=Z)NNZ-u`D!Bhy~ zG32l0KW0Jj?_Cav5efb_#{3S125B230lNOckpS!9`}6kufXrZv(ih_v=Lg$ws$vRJ zMj?EseI`i>>VL7i9Y;Z=rNs(4+>SB7&q9C4LHL0zK^Y_Q1Na>WG{uZ70gP{e`gCaV%C0=PrRNZhK2aO)Zn1SItP)Gq^xM@I%_zl3?|IV1j)5UFaxqcYYH7Q}oh8)*%Og{PT_Eha+kwE@_8v+WYUs*|l4fRto(D*Mw5WlY! zw1kJo-)DtDnF9a_phA5s6oC>QOh^g|Vg-hi{>9{1hW|MJUA_MW;jQNXEBwEZ|6d68 z1)1v~E%0C9e=YF;)&lZBzd}X_e}$~TSRsn+pCXpORUC_?n#mOgi;TzY@S-Cm`d*d8 zV0CMxy=p;d_YragJgL93jbD-hq|y!06UXirhckeUZq}UsG2q@~jzb6&>^UY8iZYS( zp}=0P2>HhaNKT`B#Y*Tc5_{AaBSO#+zUC&H2thD!9a;SlEmZAHZww1Ez`+I7A1oRvVD|mM~}b4z()Ix>QQRHaTzl^ zVVtjrIQA8It4cz+p4-e&I9w?~@DiC*rgIcyd>bLpkP05)&?0iWj^wvKG`-4ulcCEC^wRuN@GU@ zrs!T6hB_IsuEw5RaQD)a4OH81cERzWIJ4G##;8XWDP~&ableA0MZ}%5lO`fYs_NKL|NYb$d zExzrPf%Y>pVY|TTc*QF}jWC`2YTP*49zn1@T`4TvB1?GV4e~*6+Q-BUtF4#3Drs?4 zMAM4Ro<(O{7&#SB>j9TunJ!wy28Wz8weJMO}dj5p0S57+Z!&0rN9zH@& z@$;8x8Uum`JJ;{irz#Ne=_e9~45sby7myxLB8EAqWQjf63#9d7mtxC?aD z)iy0a1d?juX!>ft-p1hDa$KpgjmJtIU}dSKUMJz9VvV9QY9 zuX8|!Gw8G~NFa=8m-kO=73D=(NYsbrg)53Cky*SCz5i}HohD0O@Dk+Zu1J_mm^5B+8U?QVNMJ zN`8fsVZB3zp(e_ALOkI&Hh?;M_5dE8jP*&Nkbow(5K#GlYNy1%7(D*2FDm81lJ{W znI$A4nl0H}0)&*|f@_EZcytK5HHb$;)#grDrn*V<7>G7s+Rst!o;-Y}VO_p^N>ywf z6sij)#EOE7FKx__N9>~Hj7UFKLq-u`9nzjW-=sbw53Uyo1>&EJTGN+a7sXnhv<6Uq zau&Q_?T))-4`UZ1N!bE7!e#URt=K2%i#2{7j4cM8s6Kn%kHy^tdxUVqY$|u5UVV<8 zd}>YvcoFK=8^EWAu+=tvG!SQ!S_EqEV60ER&Y5xiC4EmRcK~vHS!YQz`}iyOIY7897`xKr1+ru1Xl={8r$76~+mIugxdy@(eqQ-b^y_!1&JAj@RnWn%!;OM0$n zYYtnmY!?tM)Veo-Fz+%O{8AmuM;T`qdl5rSC0{PJ^rcS1j|?sj9y`8MCQr?|v|(RL z8NV)O5a>EH`N9RZ_SFl#_v4BCpg)@fx^M^fmpI}$n21XVp|6w^8s4*k9EpOG@h@#ktJqqjM?&BZlU;E|C{E!jd$ z@4`N9e@O{jB2`M)Dh|%~!xUzR*RQhfW}?E9Vo{35hN6?n_!8TKGpzlXRPXv+Aaqa7 zFzMYH$LJTsJYGg_F;{+(*V5~^edC!U2kd~e#WsyK5Gx?^$Q0FI;J`}4uVkz^UfmOb z3_+fBqN5h-rD5Dl6sY#2tMygN=+cYZt7do;MSNQrMG|;EQwa({#WIbu+6uzwR&i*k z$U~x2$oR66fNM=qI0j`pr3;+zUAv`J(#nlt+pMkzk7{xIswbLSQPND3mz?i|A%$I` zXlnRHq99>0`mloZqMyk`D%LAMJU^*0xJP7S2t)fx<(cVvH5ChjjRTZc{r6h*3FC?X)zo2YaJq{S;p6AVI-4q`wA2}m*Y zE*%L-6NTiD5-@-S1VV?<3{{FiK$_AtB!L(I|Ghi!&Yd~CXJ+T@?(FQh=bW8$c4G-0 zB$?LbGat#B^4PA}2j&?thaWe_&R*uqDMmr`XPaQ$WuxnYq~`{{3Kw@{;Xr*0*Cxe; z4UbCIj3g`@$+~&#ksbPexDjVUqi*m0H)1RzMZ4E$espVnUJ4frWS5-CQkl|mdaF|Q zwofC()~7|V{8@P~c>d1S!H&1qvxne6K#L`cn4X{9ozLM`b2aMwj4M^^XoFx{1C)#N zY~@E+d8Qjw3h#Q=4IT}#{WK2pN-Ok%0{qOf4(XgDG{0td zqovZ(h4Vx$0C@LFIe(#>YQNTvgBWndvp;s-QYJxL<7sGrDIhu3)j{REz9|@E^`g}* zJxKV~b>U<-v0Jxv40Q%MC!}zzbM$YFL#OA?NY+bdGH0a+WlB;?N25-=yfX_se``;R z**~67F+J>lnaI_e9TRtG;Uc@*EmMt)LMU4bgG!=-lJXewU*+sCh^omcb5a)mZ-}a& zNO}O$xrj59pPI1n%YPf6sf!DtJDgUp=Y`=!VQv6qf+zQR^-8)OP=srx^z^qF1G~*>ybbGU5XaRn)B^Rj}2#KvX{u9JcB8`0JH@Os&S5W58oyYtm|hJs;nN zb)smIhm5!b>@}Z`uvaIa*fiH3i)KQhG?O-A+~Y+I_n0OK{zST>{JVy8Jr{f22)$&P z8m^fY{CUDtOeg)3IDj`lesyfsoK;PEQmJMJ>dpT2^;tuyD$a~NRXcmpgM2@8b`f4V zm;8cH<3myKvo-B23GJoD+1GrW9q15h2*aF@JnpNJXRj4d;e|Fx-w59QvY4!v#-=X} zfO4?9zj_E87!;jbMH?&<6Y>%aLZoaz54INXJm7ufeecwVh)ioq_aM{0$|3Fzqg*av z&QV7&!5`M zc()Fs^TVSnU`ZQhE>HEBYu5IZu+(aMhiXHsQi`m^m44;bTl?kM-OyLrlP9x>`K03Y z=k=dUs{+O=6fP8`)>M@$na~(+(sk%DK$wg$VC7XW35oW#5Ds}$@>v-=!`EMJsvT21 zFFn4Hl~|JHdu}<+PNF(jQ`2Ax3c{C}J~Z)BGkiYi;c~+0J2E~BU&4)r}kuX)KtnijBO)E11k{E1{=D_ow*|{@(y9$SHg^dcE_cMA!6?=@{hXf z^H_Yn2zouqbu+Rx!m^oD$i1(ou_pTc8BWKp8vwZ0G~*m5jzky>S$4d4aJV{VSMnvL zU|=?-?)GzI6P3%w!1fQX#0S;)q7zxm({(Xh8BHd{2LBnco0=mp5xW!bRQjPjy(?bf zl`9w1TC{Igb8hZ6ciu5KMhXkj4F!r&`!2c&c z_PnAkr1+bHRn-+!bry-aaQq4r{A%9{>oFjz%s53v-E;UKrgZ<3z_>+Vo_K6>yQrlI z=&)_vm8C=YVU;DHSaFG`{U!G9<=lFSPfRvhqJ&WN?Ri<23oOBpWl4^q#x>JerSoHrQvb2X@HZ8a0yC}n$CQUlwY+j|WMPPHzg*z7$viKJMZ`!^ji>z}? zBhazdO9fz*x=T>p%_!io?3E>8qB~!=qKBVZ4X$>^VS@LBriVDW>g9k!=w6_pxPHI$ z+HTf^hya$1EQx?)Kw-RC$&;ul4)08U-tKgff}-O# zJjmXtW5C_P0^*4&&Pn-lkM3`I39W5bOQ+$nOz9djtaJHW)9r0hlQBw_|VnI+2kGK<|J&8uN>a zK5Wy^DR5TrQqL>b>olU-=yd{DjJ17!!&8Uwqm*Qs>ch@tA7kdOs2qIN1mSe85SI`$ zT5)gf-bXTeAquMY`mi{gbRdu3u!Bsz+stiszN3meP|}Iy_N~A7D>dvWkL#_m-P7A$ zNN;87A_>D~Dm-z>KL7D1H8Rju(39H>VHKF7y3=;?J&!Gww?mPbH_8-Tp(g*4JUuy0 zMVKE6H8O8LoOkx2x#9`(G}q$vYeGdLYX(9Q?c~1tO?sC?q=o+hWdHeVjOjb42Rf(b zVz|CX9JM?9;@3s7G?gH>DVFo_AB-5iK-|q$Kn&nRQw3~j2S=tj?b1-8M>cxnM)ccP zn$V0ih;Aj|u(2W0^J5}==1qk7kM`-aFBJ-I`wVAjX{Q&LaA;t4*qhj@Y)r(m(oZSNM4++nB1 zzj!gvQMGac_vpz(koH#o@e0YDEp6&=7mP!XdkdjDMK<1{^Wt_zh>kpN4QxG&9l6 zZrK0&&*Y3hpqH0Tx!JIBBJdd}(<3jeGnhvWPv@RFsNho%0F8?lI&76b6 zl;$_dXsh2#0s##Wjnr4aVL#F1IC7N}%w(rNG~yaOy~P%>6?fIT;bks|;=2$2+Xu;j z=Zk$a{fCiu`)z@-1*EjeF{&swvsSm3HWk!H1Ujmc27;$Cbdu1jWzjvw+Mw_zJCi;v zgIPa~Rm=aNqcVg{GWu~0(CzZjwT0`UJHDx2Pj1@Zv0uOD=BfuPwY2ikEoOgABc!#{ zslxt@7P2{`DetsE6}d=T19E$~dl5u96@T&Gy+(&JA54$mOoXCBaI+%9# zj#_LSH;JZD99et}aQzUss1b2$MVke=vb4XM1Y^>8toNW4VYjZsb1E0@uNHk^()N3= zjotj^jrwa1yL5WIlgp*O08HtNRXU>fr1I%h7cvE>^Qc#J2(q}HbNMT?z)R=4qhyTpbHNk_Wzhdfz&!P5% z=#3#A!{TaQ_rMaP2XlZJIX2JWkPy39x}Wn*R4V+!N#NyAG|{-;zABk=SHIzqfQcjP z3e`Ggck$YjsIBk2l`1j=Lk(b!Ifb9yk*%}j-`fYG6aFE7WP17Sr6M=*zkY*sWR;aG znP1iIh-qyd1Jv6YPjpy9kl$J^-`&s(#hcw3$(%=ALslUdJ(;=S@jZV=p=`(V8UO{X zpli;2BaJc@L0~J+tQGQfRl&CD9IM0({rWM0PoLMm%}uj8o7OxR-^y#|QsP$ae+;+` zPmqO5)Sj=|@?2O-TUer5a{s{C{WZHnm*X-s*N?VlV;&p$Bj@%?JHoQ4c1}#DYG^)` zp40&6IQKuUHzf6IQW0yUMICzsE22P>p)CbIH! z>Jp$sZEq-g#Y|CS^?LQP=?t*D&<4?M^fm#mI#$N&a#tbvM?%9=g~F28@>xRd!D_ zLcc_Fvx3}+rhXPtxWmv0DV~!ke{WbVw*&AB46mk(?`#^>7OvS-r#Bu@C6kOaNPCmz z2263Rj>&^kVc#iFQ0QE51}k@{{Ge-YtbHy&7qWPSgnn~tfq-I`=bZ+^{3&-)gAl#< lo@Rz3XTNje2G(hn_+Fp?tqKtoy+WHWk#&C5fArww{{YU8oXG$H diff --git a/docs/logos/delltech.jpg b/docs/logos/delltech.jpg deleted file mode 100644 index 1d6faa37d5e6f38f1000cf09034835111fc07128..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 244754 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ŋIy8vL}NbKxzo!0}WM@PH1 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»`45&$$65pAeEcuSZ`5R^x=|2B-}MUJ-#5T#nq>nJ~~xPP1sj%NeEKA zmf~lwZjwme#B|4gJVJO6{;&)ynkYxhY5UzV5fD&PT*{a{th~Cas{hfBFs*oa!p;aQ zd{L<5>xzgi)Xq#M>QBc=7>A4)b^6QrphZiQFIYj2={yTpbXApfLVVmpv>C8zxY>^2 zga{4qiqsk;`(jq-h|0E0wC_4$&L`x!w}ZX~m8h>be{nem>gH>We+LHsqnkfls}hXX z$xa#M6_;}sj9K1HAC3^Sqp6HN5kropJ`_@aGJ3=&dud^~8O7Fp_B1cnk$;gfSbzsj zX5?MD&9J7`5Z5SaUD^qrnhb@^-JL>57PQ!^Ag90^!*qK~PEZ4H zB=^9z9a1s(wDK=yufLop0^+hox%;&-!lHEIPd{`TY!PbWTm=Sr2A}XgdTa2)BuuE1 z&SugrTK6j~p!il$qojG3=_92e@oHuN7aP&T>ki?^{yjd1Dsp%D?gIo&zCHjrdyitk zsvJX&fmtl3#SCjE?(7t8+9^+yi>cG@A3nlqqXI8CpC&Bs`6YJJu?Mb1-_?M21ZJ2v zhy{wj&3d(Wk7Kgdy;q^%V|(lAqXes`iJ}+BiQMtaIpiOkopbAE?QO}`M5iG6R!vh- z?%a(56HiBjV>v2=OxzLe-}!~XW^mn9pfc1w<|KVh^^eQuN_;fy$}`sTXzl@J!&BKVx(|$yGl6TquZIz{*#0Xc;vott=q*{$siR>cHs`Pn=?TF7*^O0}#sVD}h48eaF=P#Cuq;i#oqwc<ax4&UmFw7VZvThGzw_Or7Pe zNLaP%owqhg)|9+?;lSlBypem&hb*m8B=wWgJMG7}@~$sQ$lgn;p;lJ^_(Acz;QSG$ zA8OK67V`F213K(}9G=*XqNIKA%iHgusvymNyTsc@QI5G57CO-B6<=l$%s`@<1Tzd8 ziVvlHcUZ{qa>}1~(6pkyti8*ku&0B_%x`>k{Py^*v*Cm&z>q^ljC|Q4<*3TjT^DR3 zI*~)W6xb*r4ah`E4ogzBA}*&E(N=qY6)=_JIw#kj4kagBGLAZRrm5yk=O60ov2(2< z?JnR$omBtx?sx0pt;P4*Y@&%GW;Ae-1VoYUP{uP!!GvNNyV+)wktPj;H&mZFGlZ=+ z6ZVR?RbBlU#$TOIvXZg_shsrEnK1FiPMyCArDp7o* z{af$jE~FxUmW}YdE*zu0tfi9=k1%6Y2vkAmFh6$d%x|dGt&KK>ud8ZD#bjJK0r#B< zQ?|;TFDa3a>@vEZQ|GfPH*0S^jT^d3O_@z8jzmMeIg07o0xVQc^*UQ4G<9LJs!I}I zMvMoP?Gg0j9`=#L_V(aVM_0(nxr2PHv7{X(+rOQ^HyyOVlZ`r!n;Yr+z!TgMM9z~|z?0QRt(Z0^FC z?2J~SRHs?51h=#y-sZQ0m?_%%BQ5G!EeE$5AO9<+z<_G`Abu&lZoXZ{%n)I=A%W?K zZFi)iU$d$xMmsXHOdR_mnGB8Mv;sdChhK2Ymi8c)z9wkrR?PQ?oPyVi&Ocr(U|p4(e8dzNCQueNVD&i^oy#^RqPRr zm^NOOcR3jiW1~_erSabu(j9TLzcRrj_}3#;!wSa{E`S)ak=~jz<;@X>Bv3XaAH6;` zmg$jLJZNC6-tvG_-%4*myk(T5OL+W>V_N=&Rpk?ewR#h*wFbXY_oBrTXJ8!4#j-5NDyVW_*o`KCQ(cC1O66^0Br=!#!;NP zj$kS}JuEA94@m3a~V!HVO*H z&JCV63v&&9je)K=W&$W#!PEtcSwg_GZ!=o9Lxa!7ORg0w zUS+%KTdV!5;sH$9N|?#B2+=nlzH*FRi~pWDKQ*1XpQ$5ct^0~6yw9dL<|JxaKKk zxQ5D8%%k3J9%NU0Jlf}D0k(+F6pP%dG)tGLSNYaNCivT$W-Nh3rX$I5zT@TBwciQ2ctUo!<#sU1 z#{p(vUcCy~xjx=_W3(DTYs1U%!@zgB^}n*}|GBIxJ!biSaq1hzC!xr>^D3&3&L+vI zWJ-Bg(8Enm&f-Y`^rjZO5XX$2*7^`pl$67o8>OW1=8gKW2(g`ZzRD1+N}$k&JIa0h zRv+NLd5avCzsH9A5|dQ~Pw@!LH!I#4H$)8!j&}Fhr>+2xtO#W4j@oW*o5iF&li-}+ zvft{gqlp2gJ@dsEKCS!_qsSc53~}DNX%7kPGlr*#>*1B6@aYK0>8C#_2vKmFq`H4Q z{+%F;f#K6p0=%SogL0>7G4c?_%av@gr094xDqfw5NN}mhMN`YuH+&m|d^4=I?0var zFw`X>&!+a}%=E6|MC;t_TZD(?XtI)jmn8(Z54>IW!*)zu)(kezoT$X^ zmQ`{ezGez>P_~0E99U>JhH-bj*5$FPVu(-;y;l};l?wlArS!z1cK7SiQr=X^7Yv-X zWb?E|H-;*?UVLmEJxU=pwHu{l6u&DEun^Ghe45i^Z38O7nbduIIPMxA)laR@v6|%U zvs@N?k`&k}+UK+|LSShoX&O--|1jur7{~L6fCDCxirW*uXAg0CGl2fx#W6y(kBvNs z9I5jq=0vXJ>gE&t$^;a3*)GLm+5GFhaFhwA4&%}KHP;VY?-u`UN|!PxK(`bm6UW~> zubR}2Em}asxfN9>1OKV z)f?{qwC7clHWqi?ry*F$yr#O`rc@&YEo zu3CElMq5KP#TLmJw)XwW{O9=5)iQ470)MUETRkHcBZ0> zw5yr>EriA=O4b@US;b{Vtk5~_sm%THTsYA~eyyWJ*KYu++dn z8)4{XNYvlU^VVnB%LbHWeouf5Dhy)%x^x;*cKnY8PhJAWd zD_ED~bzZ6m^panl?KT(jzy9qf8|Jn^;*ji)&<9*97Q#jq?^S7HXr(w*9|p0;2gQ++ zlHTtH>|+2K=Md%&`AJrcsEo`Kd|sgQBiimpn~OHa|%Eq39uYr0TetyjOhfh z@IGCW#Qrk`1O%6<(;MohDZ?yX4qO740rB@!rBWLU2a7r3#aY$)=z)szluZHVkCENP zYR)(y-`s_mY}pg~HfV|tB<_|pO4*MvL!PWj0A*C*57mpFvL@J{Pxh_?UM!Ot1JgHN zENfEiY&Tvk^?(>t3FM5Zj?-%uP5HQ55KaX z>Pq&)%T~w7H`Ac76Rhbjr5D`m*V4dc(ikbjlBvg753 z|9#b2c7$5Uezl{tG?cR)->&Dy|M*$-R((}ok2~pNM<}HveSmr)Uma3(iVR%}fARzn z4e!so8~dRB4rMG_BS z^TErrr}&|%_A-{}?uXf#@m#j#N0bx!jA$BKB7dDNItJ_#FO4`7rL4wie@=6fjk7V8 z{_z9U`S8^g;FeM}BLxszTYt()=-l(OzQ6>>u=PA^pjcsJ*WBK~83B-2jW;2J{lcf` z0o=_v-;RvmgR%&pt8l0Q z>G|*jaIZS)-b90DC2myB-8zndIS$wn&l*#PgfHJ&g+g_g+)nBr$lF1c<&~j=a~m6TRz4d=DMep*)H1ED zWx-L3!MDi`kV_cANBa-lF!PCEJ*3}P$W)CDveOS0Z+{pB$(QIt0~fg%0A`I>!Ojr^ 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

|h_KBJ>$TybFXXj>i!s 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 L|7GbNU#9*B6$kO& diff --git a/docs/logos/omnia-logo-transparent.png b/docs/logos/omnia-logo-transparent.png deleted file mode 100644 index f2f2bf6692020a1570d1159363b860bf6efde28d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33093 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 FzW@$X=*s{A diff --git a/docs/logos/pisa.png b/docs/logos/pisa.png deleted file mode 100644 index 58615a6383a6b28deb6e28bd31956446b4200fee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24727 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 Wdm|~y1S$&v0000 Date: Thu, 12 Mar 2026 14:51:53 +0530 Subject: [PATCH 02/10] Added L1 validation for the inputs --- .../schema/storage_config.json | 179 ++++++++++++++++++ docs/cloud_init_mounts_ansible_hld.md | 53 ++---- input/storage_config.yml | 11 +- 3 files changed, 207 insertions(+), 36 deletions(-) diff --git a/common/library/module_utils/input_validation/schema/storage_config.json b/common/library/module_utils/input_validation/schema/storage_config.json index e300410346..bd04edae59 100644 --- a/common/library/module_utils/input_validation/schema/storage_config.json +++ b/common/library/module_utils/input_validation/schema/storage_config.json @@ -82,6 +82,185 @@ "pattern": "^[a-fA-F0-9]+$" } } + }, + "mounts": { + "type": "array", + "description": "Cloud-init compatible mount configurations", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Unique identifier for this mount entry", + "pattern": "^[a-zA-Z0-9_-]+$", + "minLength": 1, + "maxLength": 64 + }, + "fs_spec": { + "type": "string", + "description": "Device name or network path (e.g., /dev/sdc, UUID=xxx, 192.168.1.100:/export/share)", + "minLength": 1 + }, + "fs_file": { + "type": "string", + "description": "Mount point path", + "pattern": "^/[a-zA-Z0-9/_.-]*$" + }, + "fs_vfstype": { + "type": "string", + "description": "Filesystem type (e.g., ext4, xfs, nfs, cifs, auto)", + "enum": ["auto", "ext2", "ext3", "ext4", "xfs", "btrfs", "nfs", "nfs4", "cifs", "tmpfs", "cephfs", "vfat", "ntfs"] + }, + "fs_mntops": { + "type": "string", + "description": "Mount options (e.g., defaults,noexec,nofail)", + "pattern": "^[a-zA-Z0-9,=._-]+$" + }, + "fs_freq": { + "type": "string", + "description": "Dump frequency (usually 0)", + "pattern": "^[0-2]$" + }, + "fs_passno": { + "type": "string", + "description": "Fsck pass number (usually 0 or 2)", + "pattern": "^[0-9]$" + }, + "roles": { + "type": "array", + "description": "List of node roles to apply this mount to", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + "uniqueItems": true + }, + "hostnames": { + "type": "array", + "description": "List of specific hostnames to apply this mount to", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9.-]+$" + }, + "uniqueItems": true + }, + "groups": { + "type": "array", + "description": "List of node groups to apply this mount to", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + "uniqueItems": true + } + }, + "required": ["name", "fs_spec", "fs_file"], + "additionalProperties": false + } + }, + "mount_default_fields": { + "type": "object", + "description": "Default mount configuration for any mount entry with less than 6 options provided", + "properties": { + "fields": { + "type": "array", + "description": "Default fstab fields [fs_vfstype, fs_mntops, fs_freq, fs_passno]", + "items": { + "type": "string" + }, + "minItems": 4, + "maxItems": 6 + }, + "roles": { + "type": "array", + "description": "List of node roles to apply these defaults to", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + "uniqueItems": true + }, + "hostnames": { + "type": "array", + "description": "List of specific hostnames to apply these defaults to", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9.-]+$" + }, + "uniqueItems": true + }, + "groups": { + "type": "array", + "description": "List of node groups to apply these defaults to", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + "uniqueItems": true + } + }, + "required": ["fields"], + "additionalProperties": false + }, + "swap": { + "type": "array", + "description": "Swap file configurations", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Unique identifier for this swap entry", + "pattern": "^[a-zA-Z0-9_-]+$", + "minLength": 1, + "maxLength": 64 + }, + "filename": { + "type": "string", + "description": "Path to the swap file to create", + "pattern": "^/[a-zA-Z0-9/_.-]+$" + }, + "size": { + "type": "string", + "description": "Size in bytes, 'auto', or human-readable format (e.g., 2G, 512M)", + "pattern": "^(auto|[0-9]+[BKMGT]?)$" + }, + "maxsize": { + "type": "string", + "description": "Maximum size in bytes or human-readable format (used with size: auto)", + "pattern": "^[0-9]+[BKMGT]?$" + }, + "roles": { + "type": "array", + "description": "List of node roles to apply this swap to", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + "uniqueItems": true + }, + "hostnames": { + "type": "array", + "description": "List of specific hostnames to apply this swap to", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9.-]+$" + }, + "uniqueItems": true + }, + "groups": { + "type": "array", + "description": "List of node groups to apply this swap to", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + "uniqueItems": true + } + }, + "required": ["name", "filename", "size"], + "additionalProperties": false + } } }, "required": [ diff --git a/docs/cloud_init_mounts_ansible_hld.md b/docs/cloud_init_mounts_ansible_hld.md index 19ac923c43..3161c8b717 100644 --- a/docs/cloud_init_mounts_ansible_hld.md +++ b/docs/cloud_init_mounts_ansible_hld.md @@ -34,17 +34,6 @@ mounts: roles: ["slurm_control_node", "slurm_node"] hostnames: [] groups: [] - - - name: "local_data" - fs_spec: "/dev/sdc" - fs_file: "/opt/data" - fs_vfstype: "ext4" - fs_mntops: "defaults,nofail" - fs_freq: "0" - fs_passno: "2" - roles: [] - hostnames: [] - groups: ["grp1"] ``` #### 1.2 Swap Configuration @@ -63,34 +52,32 @@ swap: #### 1.3 Mount Default Fields ```yaml -mount_default_fields: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] +mount_default_fields: + fields: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] + roles: [] + hostnames: [] + groups: [] ``` -### 2. Ansible Role Structure +### 2. Simplified Implementation Approach + +**New Ansible Module**: `cloud_init_mounts_config` + +This module handles all resolution logic and cloud-init configuration generation: ``` storage_generic/ +├── library/ +│ └── cloud_init_mounts_config.py # New module ├── roles/ -│ ├── storage_mounts/ -│ │ ├── tasks/ -│ │ │ ├── main.yml -│ │ │ ├── parse_pxe_mapping.yml -│ │ │ ├── resolve_targets.yml -│ │ │ ├── generate_cloud_init.yml -│ │ │ └── apply_mounts.yml -│ │ ├── templates/ -│ │ │ ├── cloud_init_mounts.yml.j2 -│ │ │ └── fstab_entry.j2 -│ │ ├── vars/ -│ │ │ └── main.yml -│ │ └── defaults/ -│ │ └── main.yml -│ └── nfs_client/ -│ └── (existing role - can be extended) -├── input/ -│ └── storage_config.yml -└── playbooks/ - └── configure_storage_mounts.yml +│ └── configure_ochani/ # Existing role - modified +│ ├── tasks/ +│ │ ├── main.yml +│ │ └── configure_mounts.yml # New task file +│ └── templates/ +│ └── cloud_init_mounts.yml.j2 +└── input/ + └── storage_config.yml ``` ### 3. Target Resolution Process (Ansible Implementation) diff --git a/input/storage_config.yml b/input/storage_config.yml index a635e19954..91a9f01f5e 100644 --- a/input/storage_config.yml +++ b/input/storage_config.yml @@ -93,8 +93,9 @@ nfs_client_params: # - groups: List of node groups to apply this mount to (e.g., ["grp1", "grp2"]). Optional # Note: If roles, hostnames, and groups are all empty or not specified, the mount will be applied to all nodes -# mount_default_fields: Default values for optional mount fields -# Must contain exactly 4 items: [fs_vfstype, fs_mntops, fs_freq, fs_passno] +# mount_default_fields: Default mount configuration for any mount entry with less than 6 options provided. +# When specified, 6 items are required and represent /etc/fstab entries. +# Default: defaults,nofail,x-systemd.after=cloud-init-network.service,_netdev. # Default: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] # swap: Swap file configuration (list of swap configurations) @@ -162,7 +163,11 @@ mounts: [] # hostnames: [] # groups: [] -mount_default_fields: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] +mount_default_fields: + fields: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] + roles: [] + hostnames: [] + groups: [] # Example swap configuration: # swap: From b2ed62f035d568daac81ce50b130810c1a92393c Mon Sep 17 00:00:00 2001 From: Jagadeesh N V Date: Thu, 12 Mar 2026 15:27:44 +0530 Subject: [PATCH 03/10] Ipdated l1 validation --- .../schema/storage_config.json | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/common/library/module_utils/input_validation/schema/storage_config.json b/common/library/module_utils/input_validation/schema/storage_config.json index bd04edae59..6f32922c80 100644 --- a/common/library/module_utils/input_validation/schema/storage_config.json +++ b/common/library/module_utils/input_validation/schema/storage_config.json @@ -155,6 +155,17 @@ } }, "required": ["name", "fs_spec", "fs_file"], + "anyOf": [ + { + "required": ["roles"] + }, + { + "required": ["hostnames"] + }, + { + "required": ["groups"] + } + ], "additionalProperties": false } }, @@ -164,11 +175,11 @@ "properties": { "fields": { "type": "array", - "description": "Default fstab fields [fs_vfstype, fs_mntops, fs_freq, fs_passno]", + "description": "When specified, 6 items are required and represent /etc/fstab entries. Default: defaults,nofail,x-systemd.after=cloud-init-network.service,_netdev.", "items": { "type": "string" }, - "minItems": 4, + "minItems": 6, "maxItems": 6 }, "roles": { @@ -200,6 +211,17 @@ } }, "required": ["fields"], + "anyOf": [ + { + "required": ["roles"] + }, + { + "required": ["hostnames"] + }, + { + "required": ["groups"] + } + ], "additionalProperties": false }, "swap": { @@ -259,6 +281,17 @@ } }, "required": ["name", "filename", "size"], + "anyOf": [ + { + "required": ["roles"] + }, + { + "required": ["hostnames"] + }, + { + "required": ["groups"] + } + ], "additionalProperties": false } } From d03fe80d1eb9213825852569b7233c3184a6992b Mon Sep 17 00:00:00 2001 From: Jagadeesh N V Date: Thu, 12 Mar 2026 23:14:44 +0530 Subject: [PATCH 04/10] Updated design to remove implementation --- docs/cloud_init_mounts_ansible_hld.md | 1009 ++++++------------------- 1 file changed, 250 insertions(+), 759 deletions(-) diff --git a/docs/cloud_init_mounts_ansible_hld.md b/docs/cloud_init_mounts_ansible_hld.md index 3161c8b717..340e67442b 100644 --- a/docs/cloud_init_mounts_ansible_hld.md +++ b/docs/cloud_init_mounts_ansible_hld.md @@ -1,18 +1,16 @@ -# High-Level Design: Cloud-Init Mounts Configuration (Ansible Implementation) +# High-Level Design: Cloud-Init Mounts Configuration ## Overview -This document describes an Ansible-based design for a flexible, cloud-init compatible mount and swap configuration system. The implementation uses Ansible roles and Jinja2 templates to manage storage mounts and swap files with fine-grained control over which nodes receive each configuration. - -**Key Constraint**: The Ansible controller does not have direct access to the NFS server or storage devices. All operations are executed on target nodes. +This document describes a flexible, cloud-init compatible mount and swap configuration system that manages storage mounts and swap files with fine-grained control over which nodes receive each configuration. ## Design Goals -1. **Ansible-Native**: Use Ansible roles, tasks, and Jinja2 templates (no Python modules) -2. **Cloud-Init Compatibility**: Generate configurations compatible with cloud-init's mounts module -3. **Granular Control**: Target specific nodes by roles, hostnames, or groups from pxe_mapping.csv -4. **Idempotency**: Support repeated executions without side effects -5. **No Controller Dependencies**: All storage operations execute on target nodes +1. **Cloud-Init Compatibility**: Generate configurations compatible with cloud-init's mounts module +2. **Granular Control**: Target specific nodes by roles, hostnames, or groups from pxe_mapping.csv +3. **Idempotency**: Support repeated executions without side effects +4. **Flexibility**: Support multiple mounts and swap configurations per host +5. **Directory**: directory creation is automatic, no need to specify it in the configuration, only setting permissions would be in runcmd ## Architecture @@ -22,6 +20,7 @@ The system accepts configuration from `storage_config.yml`: #### 1.1 Mounts Configuration +TODO: fs_ prefix to be retained? its cloud-init specific ```yaml mounts: - name: "nfs_slurm_home" # Unique identifier @@ -36,6 +35,28 @@ mounts: groups: [] ``` +**Supported Filesystem Types (`fs_vfstype`):** + +- **Network Filesystems:** + - `nfs` - Network File System (NFS v3/v4) + - `nfs4` - NFS version 4 explicitly + - `cifs` - Common Internet File System (SMB/Windows shares) + - `beegfs` - BeeGFS parallel filesystem + - `glusterfs` - GlusterFS distributed filesystem + - `lustre` - Lustre parallel distributed filesystem + +- **Local Filesystems:** + - `ext4` - Fourth Extended Filesystem (recommended for Linux) + - `ext3` - Third Extended Filesystem + - `xfs` - XFS filesystem + - `btrfs` - B-tree filesystem + - `vfat` - FAT32 filesystem + - `ntfs` - NTFS filesystem (requires ntfs-3g) + +- **Special Filesystems:** + - `tmpfs` - Temporary filesystem in RAM + - `auto` - Auto-detect filesystem type (default) + #### 1.2 Swap Configuration ```yaml @@ -59,467 +80,68 @@ mount_default_fields: groups: [] ``` -### 2. Simplified Implementation Approach - -**New Ansible Module**: `cloud_init_mounts_config` - -This module handles all resolution logic and cloud-init configuration generation: - -``` -storage_generic/ -├── library/ -│ └── cloud_init_mounts_config.py # New module -├── roles/ -│ └── configure_ochani/ # Existing role - modified -│ ├── tasks/ -│ │ ├── main.yml -│ │ └── configure_mounts.yml # New task file -│ └── templates/ -│ └── cloud_init_mounts.yml.j2 -└── input/ - └── storage_config.yml -``` - -### 3. Target Resolution Process (Ansible Implementation) +### 2. Resolution Process ``` ┌─────────────────────────────────────────────────────────────┐ -│ Stage 1: Load Configuration (Ansible Controller) │ -│ - include_vars: storage_config.yml │ -│ - Read pxe_mapping_file.csv using lookup('file') │ -│ - Parse CSV using community.general.read_csv │ +│ 1. Parse PXE Mapping │ +│ - Build hostname → (role, group, ip, mac) mapping │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ -│ Stage 2: Build PXE Mapping Dictionary (set_fact) │ -│ - Create hostname → attributes mapping │ -│ - Create role → hostnames mapping │ -│ - Create group → hostnames mapping │ +│ 2. Resolve Mount Defaults for Each Host │ +│ - Check if hostname matches mount_default_fields │ +│ roles/hostnames/groups │ +│ - Return applicable defaults or global defaults │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ -│ Stage 3: Resolve Targets for Each Mount/Swap (Jinja2) │ -│ - For each mount in mounts[] │ -│ - For each swap in swap[] │ -│ - Apply role/hostname/group filters │ -│ - Build unique hostname list per mount/swap │ +│ 3. Filter Mounts for Each Host │ +│ - For each mount: │ +│ - If roles/hostnames/groups empty +│ - Else check if hostname matches criteria │ +│ - Return list of applicable mounts per host │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ -│ Stage 4: Build Hostname-to-Mounts Mapping (set_fact) │ -│ - Invert mount→hosts to host→mounts mapping │ -│ - Each hostname gets list of applicable mount names │ -│ - Each hostname gets list of applicable swap names │ +│ 4. Filter Swap for Each Host │ +│ - Apply same filtering logic as mounts │ +│ - Validate: max 1 swap for cloud-init mode │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ -│ Stage 5: Generate Cloud-Init Config (delegate_to) │ -│ - For each target host (delegate_to: {{ item }}) │ -│ - Use Jinja2 template to generate cloud-init YAML │ -│ - Write to /etc/cloud/cloud.cfg.d/99-mounts.cfg │ -│ - OR update /etc/fstab directly │ +│ 5. Generate Cloud-Init Configuration │ +│ - Generate per-host cloud-init YAML │ +│ - Append to the cloud init yml instance wise per host │ +│ TODO: Need to check if this is instance wise is [possible or not] └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ -│ Stage 6: Apply Configuration (on target nodes) │ -│ - Run cloud-init modules apply mounts │ -│ - OR use ansible.posix.mount module directly │ -│ - Create swap files using command/shell module │ +│ 6. Apply Configuration │ +│ - This will applied in the order cloud-init executes │ +│ its modules when PXE booted │ └─────────────────────────────────────────────────────────────┘ ``` -### 4. Hostname Resolution Algorithm (Ansible/Jinja2) - -#### 4.1 PXE Mapping File Structure +### 3. PXE Mapping File -The `pxe_mapping_file.csv` is the source of truth: +The `pxe_mapping_file.csv` is the source of truth for node attributes: ```csv -HOSTNAME,MAC,IP,SERVICE_TAG,GROUP_NAME,NODE_ROLE -manager01,aa:bb:cc:dd:ee:01,192.168.1.10,SVC001,grp0,slurm_control_node -compute01,aa:bb:cc:dd:ee:02,192.168.1.11,SVC002,grp1,slurm_node -compute02,aa:bb:cc:dd:ee:03,192.168.1.12,SVC003,grp1,slurm_node -compute03,aa:bb:cc:dd:ee:04,192.168.1.13,SVC004,grp2,slurm_node -``` - -#### 4.2 Parse PXE Mapping (Ansible Task) - -```yaml -# tasks/parse_pxe_mapping.yml ---- -- name: Read PXE mapping file - set_fact: - pxe_mapping_raw: "{{ lookup('file', pxe_mapping_file_path) }}" - -- name: Parse CSV to list of dictionaries - set_fact: - pxe_mapping_list: "{{ pxe_mapping_raw | community.general.read_csv }}" - -- name: Build hostname to attributes mapping - set_fact: - pxe_hostname_map: >- - {{ - pxe_hostname_map | default({}) | combine({ - item.HOSTNAME: { - 'ip': item.IP, - 'mac': item.MAC, - 'role': item.NODE_ROLE, - 'group': item.GROUP_NAME, - 'service_tag': item.SERVICE_TAG - } - }) - }} - loop: "{{ pxe_mapping_list }}" - loop_control: - label: "{{ item.HOSTNAME }}" - -- name: Build role to hostnames mapping - set_fact: - pxe_role_map: >- - {{ - pxe_role_map | default({}) | combine({ - item.NODE_ROLE: (pxe_role_map[item.NODE_ROLE] | default([])) + [item.HOSTNAME] - }) - }} - loop: "{{ pxe_mapping_list }}" - loop_control: - label: "{{ item.HOSTNAME }}" - -- name: Build group to hostnames mapping - set_fact: - pxe_group_map: >- - {{ - pxe_group_map | default({}) | combine({ - item.GROUP_NAME: (pxe_group_map[item.GROUP_NAME] | default([])) + [item.HOSTNAME] - }) - }} - loop: "{{ pxe_mapping_list }}" - loop_control: - label: "{{ item.HOSTNAME }}" - -- name: Get all hostnames - set_fact: - all_hostnames: "{{ pxe_mapping_list | map(attribute='HOSTNAME') | list }}" -``` - -#### 4.3 Resolve Targets for Each Mount (Ansible Task) - -```yaml -# tasks/resolve_targets.yml ---- -- name: Resolve target hostnames for each mount - set_fact: - mount_targets: >- - {{ - mount_targets | default({}) | combine({ - item.name: resolve_mount_targets(item, pxe_role_map, pxe_group_map, all_hostnames) - }) - }} - loop: "{{ mounts }}" - loop_control: - label: "{{ item.name }}" - vars: - resolve_mount_targets: | - {% set targets = [] %} - {% set mount = item %} - - {# If all targeting fields are empty, use all hostnames #} - {% if not mount.roles and not mount.hostnames and not mount.groups %} - {% set targets = all_hostnames %} - {% else %} - {# Resolve roles to hostnames #} - {% for role in mount.roles | default([]) %} - {% if role in pxe_role_map %} - {% set targets = targets + pxe_role_map[role] %} - {% endif %} - {% endfor %} - - {# Add explicit hostnames #} - {% set targets = targets + (mount.hostnames | default([])) %} - - {# Resolve groups to hostnames #} - {% for group in mount.groups | default([]) %} - {% if group in pxe_group_map %} - {% set targets = targets + pxe_group_map[group] %} - {% endif %} - {% endfor %} - {% endif %} - - {# Return unique list #} - {{ targets | unique | list }} - -- name: Resolve target hostnames for each swap - set_fact: - swap_targets: >- - {{ - swap_targets | default({}) | combine({ - item.name: resolve_swap_targets(item, pxe_role_map, pxe_group_map, all_hostnames) - }) - }} - loop: "{{ swap }}" - loop_control: - label: "{{ item.name }}" - vars: - resolve_swap_targets: | - {# Same logic as mount resolution #} - {% set targets = [] %} - {% set swap_item = item %} - - {% if not swap_item.roles and not swap_item.hostnames and not swap_item.groups %} - {% set targets = all_hostnames %} - {% else %} - {% for role in swap_item.roles | default([]) %} - {% if role in pxe_role_map %} - {% set targets = targets + pxe_role_map[role] %} - {% endif %} - {% endfor %} - {% set targets = targets + (swap_item.hostnames | default([])) %} - {% for group in swap_item.groups | default([]) %} - {% if group in pxe_group_map %} - {% set targets = targets + pxe_group_map[group] %} - {% endif %} - {% endfor %} - {% endif %} - - {{ targets | unique | list }} -``` - -#### 4.4 Build Hostname-to-Mounts Mapping (Ansible Task) - -```yaml -# tasks/resolve_targets.yml (continued) ---- -- name: Build hostname to mounts mapping - set_fact: - hostname_mounts: >- - {{ - hostname_mounts | default({}) | combine({ - hostname: { - 'mounts': get_mounts_for_host(hostname, mount_targets, mounts), - 'swap': get_swap_for_host(hostname, swap_targets, swap) - } - }) - }} - loop: "{{ all_hostnames }}" - loop_control: - loop_var: hostname - vars: - get_mounts_for_host: | - {% set host_mounts = [] %} - {% for mount_name, target_hosts in mount_targets.items() %} - {% if hostname in target_hosts %} - {% set mount_config = mounts | selectattr('name', 'equalto', mount_name) | first %} - {% set host_mounts = host_mounts + [mount_config] %} - {% endif %} - {% endfor %} - {{ host_mounts }} - - get_swap_for_host: | - {% set host_swaps = [] %} - {% for swap_name, target_hosts in swap_targets.items() %} - {% if hostname in target_hosts %} - {% set swap_config = swap | selectattr('name', 'equalto', swap_name) | first %} - {% set host_swaps = host_swaps + [swap_config] %} - {% endif %} - {% endfor %} - {{ host_swaps }} +FUNCTIONAL_GROUP_NAME,GROUP_NAME,SERVICE_TAG,PARENT_SERVICE_TAG,HOSTNAME,ADMIN_MAC,ADMIN_IP,BMC_MAC,BMC_IP +slurm_control_node,grp0,SVC001,PARENT001,manager01,aa:bb:cc:dd:ee:01,192.168.1.10,aa:bb:cc:dd:ee:f1,192.168.2.10 +slurm_node,grp1,SVC002,PARENT001,compute01,aa:bb:cc:dd:ee:02,192.168.1.11,aa:bb:cc:dd:ee:f2,192.168.2.11 +slurm_node,grp1,SVC003,PARENT001,compute02,aa:bb:cc:dd:ee:03,192.168.1.12,aa:bb:cc:dd:ee:f3,192.168.2.12 +slurm_node,grp2,SVC004,PARENT001,compute03,aa:bb:cc:dd:ee:04,192.168.1.13,aa:bb:cc:dd:ee:f4,192.168.2.13 ``` -### 5. Cloud-Init Configuration Generation +**Key Fields:** +- `FUNCTIONAL_GROUP_NAME`: Node role (e.g., slurm_control_node, slurm_node) +- `GROUP_NAME`: Node group for targeting +- `HOSTNAME`: Unique hostname for the node +- `ADMIN_MAC` / `ADMIN_IP`: Admin network interface details -#### 5.1 Jinja2 Template for Cloud-Init - -```jinja2 -{# templates/cloud_init_mounts.yml.j2 #} -#cloud-config -# Generated by Omnia storage_mounts role -# Hostname: {{ inventory_hostname }} -# Generated at: {{ ansible_date_time.iso8601 }} - -{% if hostname_mounts[inventory_hostname].mounts | length > 0 %} -mounts: -{% for mount in hostname_mounts[inventory_hostname].mounts %} - - ["{{ mount.fs_spec }}", "{{ mount.fs_file }}", "{{ mount.fs_vfstype | default('auto') }}", "{{ mount.fs_mntops | default('defaults,nofail') }}", "{{ mount.fs_freq | default('0') }}", "{{ mount.fs_passno | default('2') }}"] -{% endfor %} - -mount_default_fields: {{ mount_default_fields | to_json }} -{% endif %} - -{% if hostname_mounts[inventory_hostname].swap | length > 0 %} -{% set swap_config = hostname_mounts[inventory_hostname].swap[0] %} -swap: - filename: {{ swap_config.filename }} - size: {{ swap_config.size }} -{% if swap_config.maxsize %} - maxsize: {{ swap_config.maxsize }} -{% endif %} -{% endif %} -``` - -#### 5.2 Generate and Apply Cloud-Init Configuration - -```yaml -# tasks/generate_cloud_init.yml ---- -- name: Create cloud-init configuration directory - file: - path: /etc/cloud/cloud.cfg.d - state: directory - mode: '0755' - delegate_to: "{{ item }}" - loop: "{{ all_hostnames }}" - when: hostname_mounts[item].mounts | length > 0 or hostname_mounts[item].swap | length > 0 - -- name: Generate cloud-init mounts configuration - template: - src: cloud_init_mounts.yml.j2 - dest: /etc/cloud/cloud.cfg.d/99-mounts.cfg - mode: '0644' - delegate_to: "{{ item }}" - loop: "{{ all_hostnames }}" - when: hostname_mounts[item].mounts | length > 0 or hostname_mounts[item].swap | length > 0 - notify: Apply cloud-init mounts - -- name: Apply cloud-init mounts module - command: cloud-init single --name mounts - delegate_to: "{{ item }}" - loop: "{{ all_hostnames }}" - when: - - hostname_mounts[item].mounts | length > 0 or hostname_mounts[item].swap | length > 0 - - apply_cloud_init | default(true) -``` - -### 6. Alternative: Direct Mount Management (Without Cloud-Init) - -For environments where cloud-init is not available or preferred: - -```yaml -# tasks/apply_mounts.yml ---- -- name: Mount filesystems directly using ansible.posix.mount - ansible.posix.mount: - path: "{{ mount_item.fs_file }}" - src: "{{ mount_item.fs_spec }}" - fstype: "{{ mount_item.fs_vfstype | default('auto') }}" - opts: "{{ mount_item.fs_mntops | default('defaults,nofail') }}" - dump: "{{ mount_item.fs_freq | default('0') }}" - passno: "{{ mount_item.fs_passno | default('2') }}" - state: mounted - delegate_to: "{{ hostname }}" - loop: "{{ hostname_mounts[hostname].mounts }}" - loop_control: - loop_var: mount_item - label: "{{ mount_item.name }}" - when: hostname_mounts[hostname].mounts | length > 0 - with_items: "{{ all_hostnames }}" - loop_control: - loop_var: hostname - -- name: Create swap file - command: | - dd if=/dev/zero of={{ swap_item.filename }} bs=1M count={{ swap_item.size | regex_replace('[^0-9]', '') }} - args: - creates: "{{ swap_item.filename }}" - delegate_to: "{{ hostname }}" - loop: "{{ hostname_mounts[hostname].swap }}" - loop_control: - loop_var: swap_item - label: "{{ swap_item.name }}" - when: hostname_mounts[hostname].swap | length > 0 - with_items: "{{ all_hostnames }}" - loop_control: - loop_var: hostname - -- name: Set swap file permissions - file: - path: "{{ swap_item.filename }}" - mode: '0600' - delegate_to: "{{ hostname }}" - loop: "{{ hostname_mounts[hostname].swap }}" - loop_control: - loop_var: swap_item - label: "{{ swap_item.name }}" - when: hostname_mounts[hostname].swap | length > 0 - with_items: "{{ all_hostnames }}" - loop_control: - loop_var: hostname - -- name: Format swap file - command: mkswap {{ swap_item.filename }} - delegate_to: "{{ hostname }}" - loop: "{{ hostname_mounts[hostname].swap }}" - loop_control: - loop_var: swap_item - label: "{{ swap_item.name }}" - when: hostname_mounts[hostname].swap | length > 0 - with_items: "{{ all_hostnames }}" - loop_control: - loop_var: hostname - -- name: Enable swap - command: swapon {{ swap_item.filename }} - delegate_to: "{{ hostname }}" - loop: "{{ hostname_mounts[hostname].swap }}" - loop_control: - loop_var: swap_item - label: "{{ swap_item.name }}" - when: hostname_mounts[hostname].swap | length > 0 - with_items: "{{ all_hostnames }}" - loop_control: - loop_var: hostname - -- name: Add swap to /etc/fstab - lineinfile: - path: /etc/fstab - line: "{{ swap_item.filename }} none swap sw 0 0" - state: present - delegate_to: "{{ hostname }}" - loop: "{{ hostname_mounts[hostname].swap }}" - loop_control: - loop_var: swap_item - label: "{{ swap_item.name }}" - when: hostname_mounts[hostname].swap | length > 0 - with_items: "{{ all_hostnames }}" - loop_control: - loop_var: hostname -``` - -### 7. Main Playbook - -```yaml -# playbooks/configure_storage_mounts.yml ---- -- name: Configure Storage Mounts and Swap - hosts: localhost - gather_facts: false - vars_files: - - ../input/storage_config.yml - vars: - pxe_mapping_file_path: "/path/to/pxe_mapping_file.csv" - apply_cloud_init: true # Set to false to use direct mount management - - tasks: - - name: Parse PXE mapping file - include_tasks: ../roles/storage_mounts/tasks/parse_pxe_mapping.yml - - - name: Resolve mount and swap targets - include_tasks: ../roles/storage_mounts/tasks/resolve_targets.yml - - - name: Display hostname to mounts mapping - debug: - var: hostname_mounts - verbosity: 1 - - - name: Generate and apply cloud-init configuration - include_tasks: ../roles/storage_mounts/tasks/generate_cloud_init.yml - when: apply_cloud_init | default(true) - - - name: Apply mounts directly (without cloud-init) - include_tasks: ../roles/storage_mounts/tasks/apply_mounts.yml - when: not (apply_cloud_init | default(true)) -``` - -### 8. Data Flow Example +### 4. Data Flow Example #### Input Configuration @@ -531,362 +153,231 @@ mounts: fs_file: "/home" fs_vfstype: "nfs" fs_mntops: "defaults,nofail,_netdev" - fs_freq: "0" - fs_passno: "0" roles: ["slurm_control_node", "slurm_node"] - hostnames: [] - groups: [] - - - name: "local_data" - fs_spec: "/dev/sdc" - fs_file: "/opt/data" - fs_vfstype: "ext4" - fs_mntops: "defaults,nofail" - fs_freq: "0" - fs_passno: "2" - roles: [] - hostnames: [] - groups: ["grp1"] swap: - name: "compute_swap" filename: "/swapfile" size: "4G" - maxsize: "8G" roles: ["slurm_node"] - hostnames: [] - groups: [] -mount_default_fields: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] +mount_default_fields: + fields: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] + roles: [] ``` -#### PXE Mapping Data +#### Resolution for compute01 -```csv -HOSTNAME,MAC,IP,SERVICE_TAG,GROUP_NAME,NODE_ROLE -manager01,aa:bb:cc:dd:ee:01,192.168.1.10,SVC001,grp0,slurm_control_node -compute01,aa:bb:cc:dd:ee:02,192.168.1.11,SVC002,grp1,slurm_node -compute02,aa:bb:cc:dd:ee:03,192.168.1.12,SVC003,grp1,slurm_node -compute03,aa:bb:cc:dd:ee:04,192.168.1.13,SVC004,grp2,slurm_node -``` - -#### Ansible Facts Generated +Given PXE mapping shows: `compute01` has `role=slurm_node`, `group=grp1` -```yaml -# pxe_role_map -pxe_role_map: - slurm_control_node: ["manager01"] - slurm_node: ["compute01", "compute02", "compute03"] - -# pxe_group_map -pxe_group_map: - grp0: ["manager01"] - grp1: ["compute01", "compute02"] - grp2: ["compute03"] - -# mount_targets -mount_targets: - nfs_slurm_home: ["manager01", "compute01", "compute02", "compute03"] - local_data: ["compute01", "compute02"] - -# swap_targets -swap_targets: - compute_swap: ["compute01", "compute02", "compute03"] - -# hostname_mounts -hostname_mounts: - manager01: - mounts: - - name: "nfs_slurm_home" - fs_spec: "172.16.107.168:/mnt/share/omnia" - fs_file: "/home" - fs_vfstype: "nfs" - fs_mntops: "defaults,nofail,_netdev" - fs_freq: "0" - fs_passno: "0" - swap: [] - - compute01: - mounts: - - name: "nfs_slurm_home" - fs_spec: "172.16.107.168:/mnt/share/omnia" - fs_file: "/home" - fs_vfstype: "nfs" - fs_mntops: "defaults,nofail,_netdev" - fs_freq: "0" - fs_passno: "0" - - name: "local_data" - fs_spec: "/dev/sdc" - fs_file: "/opt/data" - fs_vfstype: "ext4" - fs_mntops: "defaults,nofail" - fs_freq: "0" - fs_passno: "2" - swap: - - name: "compute_swap" - filename: "/swapfile" - size: "4G" - maxsize: "8G" - - compute02: - mounts: - - name: "nfs_slurm_home" - fs_spec: "172.16.107.168:/mnt/share/omnia" - fs_file: "/home" - fs_vfstype: "nfs" - fs_mntops: "defaults,nofail,_netdev" - fs_freq: "0" - fs_passno: "0" - - name: "local_data" - fs_spec: "/dev/sdc" - fs_file: "/opt/data" - fs_vfstype: "ext4" - fs_mntops: "defaults,nofail" - fs_freq: "0" - fs_passno: "2" - swap: - - name: "compute_swap" - filename: "/swapfile" - size: "4G" - maxsize: "8G" - - compute03: - mounts: - - name: "nfs_slurm_home" - fs_spec: "172.16.107.168:/mnt/share/omnia" - fs_file: "/home" - fs_vfstype: "nfs" - fs_mntops: "defaults,nofail,_netdev" - fs_freq: "0" - fs_passno: "0" - swap: - - name: "compute_swap" - filename: "/swapfile" - size: "4G" - maxsize: "8G" -``` +**Applicable Configuration:** +- Mount: `nfs_slurm_home` (matches role `slurm_node`) +- Swap: `compute_swap` (matches role `slurm_node`) #### Generated Cloud-Init for compute01 ```yaml #cloud-config -# Generated by Omnia storage_mounts role # Hostname: compute01 -# Generated at: 2026-03-12T13:28:00Z mounts: - ["172.16.107.168:/mnt/share/omnia", "/home", "nfs", "defaults,nofail,_netdev", "0", "0"] - - ["/dev/sdc", "/opt/data", "ext4", "defaults,nofail", "0", "2"] mount_default_fields: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] swap: filename: /swapfile size: 4G - maxsize: 8G ``` -## Key Design Decisions - -### 1. Ansible-Native Implementation -- **No Python modules**: All logic implemented using Ansible tasks, Jinja2 templates, and filters -- **Controller independence**: No direct access to NFS/storage required from controller -- **delegate_to**: All storage operations execute on target nodes - -### 2. PXE Mapping as Source of Truth -- All hostname, role, and group data sourced from `pxe_mapping_file.csv` -- Parsed once at playbook start using `community.general.read_csv` -- Built into efficient lookup dictionaries using `set_fact` - -### 3. Two Deployment Modes -- **Cloud-Init Mode**: Generate cloud-init configuration files -- **Direct Mode**: Use `ansible.posix.mount` module directly -- Configurable via `apply_cloud_init` variable - -### 4. Idempotency -- Cloud-init configurations are idempotent by design -- `ansible.posix.mount` module ensures idempotent mount operations -- Swap file creation uses `creates` parameter to avoid recreation - -### 5. Flexibility -- Support for multiple mounts per host -- Support for multiple swap files per host -- Union logic for roles, hostnames, and groups - -## Implementation Considerations - -### 1. Performance Optimization - -```yaml -# Use async for parallel execution on multiple hosts -- name: Apply mounts on all hosts in parallel - ansible.posix.mount: - path: "{{ mount_item.fs_file }}" - src: "{{ mount_item.fs_spec }}" - fstype: "{{ mount_item.fs_vfstype }}" - state: mounted - delegate_to: "{{ hostname }}" - async: 300 - poll: 0 - register: mount_async - loop: "{{ all_hostnames }}" - loop_control: - loop_var: hostname - -- name: Wait for mount operations to complete - async_status: - jid: "{{ item.ansible_job_id }}" - register: mount_result - until: mount_result.finished - retries: 30 - delay: 10 - loop: "{{ mount_async.results }}" +### 5. Validation Requirements + +#### Level 1: Schema Validation (JSON Schema) +- ✅ Data types and formats +- ✅ Required fields (name, fs_spec, fs_file for mounts; name, filename, size for swap) +- ✅ Path patterns (absolute paths starting with `/`) +- ✅ Size format (auto, 4G, 512M, etc.) +- ✅ Filesystem types (enum of supported types) +- ✅ Unique items in arrays + +#### Level 2: Business Logic Validation +- **Unique mount names** across all mounts +- **Unique swap names** across all swaps +- **Hostname existence** in PXE mapping +- **Swap filename uniqueness** per host +- **Max 1 swap** per host for cloud-init mode +- **Mount point uniqueness** per host +- **No circular dependencies** (e.g., swap on tmpfs) + +#### JSON Schema + +The configuration is validated using JSON Schema located at: +`/new_omnia/omnia/ansible_collections/dell/storage_generic/common/library/module_utils/input_validation/schema/storage_config.json` + +**Key Schema Validations for Mounts:** +```json +{ + "mounts": { + "type": "array", + "items": { + "properties": { + "name": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "minLength": 1, + "maxLength": 64 + }, + "fs_spec": { + "type": "string", + "minLength": 1 + }, + "fs_file": { + "type": "string", + "pattern": "^/[a-zA-Z0-9/_.-]*$" + }, + "fs_vfstype": { + "type": "string", + "enum": ["auto", "ext2", "ext3", "ext4", "xfs", "btrfs", + "nfs", "nfs4", "cifs", "tmpfs", "cephfs", "vfat", "ntfs"] + }, + "fs_mntops": { + "type": "string", + "pattern": "^[a-zA-Z0-9,=._-]+$" + }, + "fs_freq": { + "type": "string", + "pattern": "^[0-2]$" + }, + "fs_passno": { + "type": "string", + "pattern": "^[0-9]$" + }, + "roles": { + "type": "array", + "items": {"type": "string", "pattern": "^[a-zA-Z0-9_-]+$"}, + "uniqueItems": true + }, + "hostnames": { + "type": "array", + "items": {"type": "string", "pattern": "^[a-zA-Z0-9.-]+$"}, + "uniqueItems": true + }, + "groups": { + "type": "array", + "items": {"type": "string", "pattern": "^[a-zA-Z0-9_-]+$"}, + "uniqueItems": true + } + }, + "required": ["name", "fs_spec", "fs_file"] + } + } +} ``` -### 2. Error Handling - -```yaml -- name: Validate mount configuration - assert: - that: - - item.name is defined - - item.fs_spec is defined - - item.fs_file is defined - fail_msg: "Mount configuration missing required fields" - loop: "{{ mounts }}" - loop_control: - label: "{{ item.name | default('unnamed') }}" - -- name: Check if NFS server is reachable (from target nodes) - wait_for: - host: "{{ item.fs_spec.split(':')[0] }}" - port: 2049 - timeout: 10 - delegate_to: "{{ hostname }}" - when: item.fs_vfstype == 'nfs' - ignore_errors: true - register: nfs_check +**Key Schema Validations for Swap:** +```json +{ + "swap": { + "type": "array", + "items": { + "properties": { + "name": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "minLength": 1, + "maxLength": 64 + }, + "filename": { + "type": "string", + "pattern": "^/[a-zA-Z0-9/_.-]+$" + }, + "size": { + "type": "string", + "pattern": "^(auto|[0-9]+[BKMGT]?)$" + }, + "maxsize": { + "type": "string", + "pattern": "^[0-9]+[BKMGT]?$" + }, + "roles": { + "type": "array", + "items": {"type": "string", "pattern": "^[a-zA-Z0-9_-]+$"}, + "uniqueItems": true + }, + "hostnames": { + "type": "array", + "items": {"type": "string", "pattern": "^[a-zA-Z0-9.-]+$"}, + "uniqueItems": true + }, + "groups": { + "type": "array", + "items": {"type": "string", "pattern": "^[a-zA-Z0-9_-]+$"}, + "uniqueItems": true + } + }, + "required": ["name", "filename", "size"] + } + } +} ``` -### 3. Validation - -```yaml -- name: Validate unique mount names - assert: - that: - - mounts | map(attribute='name') | list | length == mounts | map(attribute='name') | unique | list | length - fail_msg: "Duplicate mount names found" - -- name: Validate hostnames exist in PXE mapping - assert: - that: - - item in all_hostnames - fail_msg: "Hostname {{ item }} not found in pxe_mapping.csv" - loop: "{{ mounts | map(attribute='hostnames') | flatten | unique }}" - when: item | length > 0 +**Key Schema Validations for Mount Default Fields:** +```json +{ + "mount_default_fields": { + "type": "object", + "properties": { + "fields": { + "type": "array", + "items": {"type": "string"}, + "minItems": 6, + "maxItems": 6 + }, + "roles": { + "type": "array", + "items": {"type": "string", "pattern": "^[a-zA-Z0-9_-]+$"}, + "uniqueItems": true + }, + "hostnames": { + "type": "array", + "items": {"type": "string", "pattern": "^[a-zA-Z0-9.-]+$"}, + "uniqueItems": true + }, + "groups": { + "type": "array", + "items": {"type": "string", "pattern": "^[a-zA-Z0-9_-]+$"}, + "uniqueItems": true + } + }, + "required": ["fields"] + } +} ``` -### 4. Logging and Debugging +### 6. Key Design Decisions -```yaml -- name: Log mount resolution - debug: - msg: | - Mount: {{ item.key }} - Targets: {{ item.value | join(', ') }} - loop: "{{ mount_targets | dict2items }}" - loop_control: - label: "{{ item.key }}" - -- name: Create mount application log - copy: - content: | - Hostname: {{ inventory_hostname }} - Mounts Applied: {{ hostname_mounts[inventory_hostname].mounts | map(attribute='name') | join(', ') }} - Swap Applied: {{ hostname_mounts[inventory_hostname].swap | map(attribute='name') | join(', ') }} - Timestamp: {{ ansible_date_time.iso8601 }} - dest: /var/log/omnia_mounts.log - delegate_to: "{{ item }}" - loop: "{{ all_hostnames }}" -``` +1. **PXE Mapping as Source of Truth** + - All hostname, role, and group data from `pxe_mapping_file.csv` + - Single source for node attributes -## Integration with Existing Roles +2. **Mount Default Fields with Targeting** + - Supports roles, hostnames, groups + - Allows different defaults for different node types + - Falls back to global defaults -### Option 1: Extend nfs_client Role +3. **Cloud-Init Constraint** + - Max 1 swap file per host (cloud-init limitation) + - Multiple mounts supported per host -```yaml -# roles/nfs_client/tasks/main.yml ---- -- name: Include cloud-init mounts configuration - include_tasks: cloud_init_mounts.yml - when: use_cloud_init_mounts | default(false) - -- name: Traditional NFS client setup - include_tasks: traditional_nfs.yml - when: not (use_cloud_init_mounts | default(false)) -``` +4. **Targeting Logic** + - Empty roles/hostnames/groups = apply to all hosts + - Union of all specified criteria + - Unique hostname list per mount/swap -### Option 2: Create New storage_mounts Role - -```yaml -# Create dedicated role for mount management -# Can be called from existing playbooks -- name: Configure storage mounts - include_role: - name: storage_mounts - vars: - storage_config_file: "{{ omnia_input_path }}/storage_config.yml" - pxe_mapping_file: "{{ omnia_input_path }}/pxe_mapping_file.csv" -``` - -## Security Considerations - -1. **Credential Management**: - - CIFS credentials stored in `/root/.smbcreds` with 0600 permissions - - Use Ansible Vault for sensitive mount options - -2. **Mount Options**: - - Always use `nofail` to prevent boot failures - - Use `_netdev` for network filesystems - - Use `nosuid,nodev,noexec` where appropriate - -3. **Validation**: - - Validate all paths to prevent directory traversal - - Sanitize user inputs from storage_config.yml - - Check filesystem types against allowed list - -4. **Permissions**: - - Swap files created with 0600 permissions - - Mount points created with appropriate ownership - -## Testing Strategy - -```yaml -# Test playbook ---- -- name: Test storage mounts configuration - hosts: localhost - tasks: - - name: Validate configuration syntax - include_role: - name: storage_mounts - tasks_from: validate - - - name: Dry run - show what would be mounted - include_role: - name: storage_mounts - vars: - check_mode: true - - - name: Apply to single test host - include_role: - name: storage_mounts - vars: - limit_hosts: ["compute01"] -``` +5. **Idempotency** + - Cloud-init configurations are idempotent by design + - Repeated executions produce same result ## Conclusion -This Ansible-based design provides a flexible, maintainable approach to managing storage mounts and swap configurations without requiring Python modules or controller access to storage systems. By leveraging Ansible's native capabilities (tasks, templates, filters, and delegation), the system can efficiently manage mounts across heterogeneous clusters while maintaining idempotency and providing clear audit trails. - -The design supports both cloud-init and direct mount management approaches, allowing deployment flexibility based on environment requirements. +This design provides a flexible, cloud-init compatible mount and swap configuration system with granular per-host control through roles, hostnames, and groups targeting. The system resolves configurations from `storage_config.yml` and `pxe_mapping_file.csv` to generate instance-specific cloud-init configurations that are applied on each target node. From 038423c1cbb86f1fb6320ba6e5673daae70bab51 Mon Sep 17 00:00:00 2001 From: Jagadeesh N V Date: Fri, 20 Mar 2026 12:23:19 +0530 Subject: [PATCH 05/10] Updated design - better names for the mount fields --- .../schema/storage_config.json | 41 +++++---- input/storage_config.yml | 84 ++++++++++--------- 2 files changed, 71 insertions(+), 54 deletions(-) diff --git a/common/library/module_utils/input_validation/schema/storage_config.json b/common/library/module_utils/input_validation/schema/storage_config.json index 6f32922c80..4bb924c608 100644 --- a/common/library/module_utils/input_validation/schema/storage_config.json +++ b/common/library/module_utils/input_validation/schema/storage_config.json @@ -96,32 +96,32 @@ "minLength": 1, "maxLength": 64 }, - "fs_spec": { + "source": { "type": "string", "description": "Device name or network path (e.g., /dev/sdc, UUID=xxx, 192.168.1.100:/export/share)", "minLength": 1 }, - "fs_file": { + "mount_point": { "type": "string", "description": "Mount point path", "pattern": "^/[a-zA-Z0-9/_.-]*$" }, - "fs_vfstype": { + "fs_type": { "type": "string", "description": "Filesystem type (e.g., ext4, xfs, nfs, cifs, auto)", "enum": ["auto", "ext2", "ext3", "ext4", "xfs", "btrfs", "nfs", "nfs4", "cifs", "tmpfs", "cephfs", "vfat", "ntfs"] }, - "fs_mntops": { + "mnt_opts": { "type": "string", "description": "Mount options (e.g., defaults,noexec,nofail)", "pattern": "^[a-zA-Z0-9,=._-]+$" }, - "fs_freq": { + "dump_freq": { "type": "string", "description": "Dump frequency (usually 0)", "pattern": "^[0-2]$" }, - "fs_passno": { + "fsck_pass": { "type": "string", "description": "Fsck pass number (usually 0 or 2)", "pattern": "^[0-9]$" @@ -173,14 +173,25 @@ "type": "object", "description": "Default mount configuration for any mount entry with less than 6 options provided", "properties": { - "fields": { - "type": "array", - "description": "When specified, 6 items are required and represent /etc/fstab entries. Default: defaults,nofail,x-systemd.after=cloud-init-network.service,_netdev.", - "items": { - "type": "string" - }, - "minItems": 6, - "maxItems": 6 + "fs_type": { + "type": "string", + "description": "Filesystem type (e.g., ext4, xfs, nfs, cifs, auto)", + "enum": ["auto", "ext2", "ext3", "ext4", "xfs", "btrfs", "nfs", "nfs4", "cifs", "tmpfs", "cephfs", "vfat", "ntfs"] + }, + "mnt_opts": { + "type": "string", + "description": "Mount options (e.g., defaults,noexec,nofail)", + "pattern": "^[a-zA-Z0-9,=._-]+$" + }, + "dump_freq": { + "type": "string", + "description": "Dump frequency (usually 0)", + "pattern": "^[0-2]$" + }, + "fsck_pass": { + "type": "string", + "description": "Fsck pass number (usually 0 or 2)", + "pattern": "^[0-9]$" }, "roles": { "type": "array", @@ -210,7 +221,7 @@ "uniqueItems": true } }, - "required": ["fields"], + "required": ["fs_type", "mnt_opts", "dump_freq", "fsck_pass"], "anyOf": [ { "required": ["roles"] diff --git a/input/storage_config.yml b/input/storage_config.yml index 91a9f01f5e..8f387f8d40 100644 --- a/input/storage_config.yml +++ b/input/storage_config.yml @@ -80,23 +80,29 @@ nfs_client_params: # Each mount entry contains the following fields (matching /etc/fstab format): # - name: Unique identifier for this mount entry (e.g., "data_mount", "nfs_home", "swap_storage"). Required -# - fs_spec: Device name (e.g., /dev/sdc, ephemeral0, UUID=xxx, or network path like 192.168.1.100:/export/share) -# - fs_file: Mount point path (e.g., /mnt, /opt/data) -# - fs_vfstype: Filesystem type (e.g., ext4, xfs, nfs, cifs, auto), supported by mount module +# - source: Device name (e.g., /dev/sdc, ephemeral0, UUID=xxx, or network path like 192.168.1.100:/export/share) +# - mount_point: Mount point path (e.g., /mnt, /opt/data) +# - fs_type: Filesystem type (e.g., ext4, xfs, nfs, cifs, auto), supported by mount module # - nfs, tmpfs, cifs, cephfs, btrfs, ext2, ext3, ext4, xfs, etc. -# - For network filesystems (nfs, cifs, etc.), fs_spec should be the server path (e.g., 192.168.1.100:/export/share) -# - fs_mntops: Mount options (e.g., defaults,noexec,nofail). Optional, defaults to "defaults,nofail,x-systemd.after=cloud-init-network.service" -# - fs_freq: Dump frequency (usually "0"). Optional, defaults to "0" -# - fs_passno: Fsck pass number (usually "0" or "2"). Optional, defaults to "2" +# - For network filesystems (nfs, cifs, etc.), source should be the server path (e.g., 192.168.1.100:/export/share) +# - mnt_opts: Mount options (e.g., defaults,noexec,nofail). Optional, defaults to "defaults,nofail,x-systemd.after=cloud-init-network.service" +# - dump_freq: Dump frequency (usually "0"). Optional, defaults to "0" +# - fsck_pass: Fsck pass number (usually "0" or "2"). Optional, defaults to "2" # - roles: List of node roles to apply this mount to (e.g., ["slurm_control_node", "slurm_node", "login_node"]). Optional # - hostnames: List of specific hostnames to apply this mount to (e.g., ["node01", "node02"]). Optional # - groups: List of node groups to apply this mount to (e.g., ["grp1", "grp2"]). Optional # Note: If roles, hostnames, and groups are all empty or not specified, the mount will be applied to all nodes -# mount_default_fields: Default mount configuration for any mount entry with less than 6 options provided. -# When specified, 6 items are required and represent /etc/fstab entries. -# Default: defaults,nofail,x-systemd.after=cloud-init-network.service,_netdev. -# Default: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] +# mount_default_fields: +# - fields: Default mount configuration for any mount entry with less than 6 options provided. +# When specified, 6 items are required and represent /etc/fstab entries. +# Default: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] +# - roles: List of node roles to apply this default to (e.g., ["slurm_node", "kube_node"]). Optional +# Example: ["slurm_node", "kube_node"] +# - hostnames: List of specific hostnames to apply this default to (e.g., ["node01", "node02"]). Optional +# Example: ["node01", "node02"] +# - groups: List of node groups to apply this default to (e.g., ["grp1", "grp2"]). Optional +# Example: ["grp1", "grp2"] # swap: Swap file configuration (list of swap configurations) # Each swap entry contains the following fields: @@ -113,52 +119,52 @@ nfs_client_params: # Example configuration: # mounts: # - name: "node_storage_mount" -# fs_spec: "/dev/sdc" -# fs_file: "/opt/data" -# fs_vfstype: "ext4" -# fs_mntops: "defaults,nofail" -# fs_freq: "0" -# fs_passno: "2" +# source: "/dev/sdc" +# mount_point: "/opt/data" +# fs_type: "ext4" +# mnt_opts: "defaults,nofail" +# dump_freq: "0" +# fsck_pass: "2" # roles: ["slurm_node"] # hostnames: [] # groups: ["grp1"] # - name: "ephemeral_mount" -# fs_spec: "ephemeral0" -# fs_file: "/mnt" -# fs_vfstype: "auto" -# fs_mntops: "defaults,noexec" +# source: "ephemeral0" +# mount_point: "/mnt" +# fs_type: "auto" +# mnt_opts: "defaults,noexec" # roles: [] # hostnames: ["node01", "node02"] # groups: [] # - name: "nfs_slurm" -# fs_spec: "192.168.1.100:/export/share" -# fs_file: "/mnt/nfs" -# fs_vfstype: "nfs" -# fs_mntops: "defaults,nofail,_netdev" -# fs_freq: "0" -# fs_passno: "0" +# source: "192.168.1.100:/export/share" +# mount_point: "/mnt/nfs" +# fs_type: "nfs" +# mnt_opts: "defaults,nofail,_netdev" +# dump_freq: "0" +# fsck_pass: "0" # roles: ["slurm_control_node", "slurm_node", "login_node"] # hostnames: [] # groups: [] # - name: "cifs_k8s" -# fs_spec: "//server/share" -# fs_file: "/mnt/cifs" -# fs_vfstype: "cifs" -# fs_mntops: "credentials=/root/.smbcreds,nofail" -# fs_freq: "0" -# fs_passno: "0" +# source: "//server/share" +# mount_point: "/mnt/cifs" +# fs_type: "cifs" +# mnt_opts: "credentials=/root/.smbcreds,nofail" +# dump_freq: "0" +# fsck_pass: "0" # roles: ['kube_control_plane', 'kube_node'] # hostnames: [] # groups: ["grp2"] mounts: [] # - name: "nfs_slurm_home" - # fs_spec: "172.16.107.168:/mnt/share/omnia" - # fs_file: "/home" - # fs_vfstype: "nfs" - # fs_mntops: "defaults,nofail,_netdev" - # fs_freq: "0" - # fs_passno: "0" + # source: "172.16.107.168:/mnt/share/omnia" + # mount_point: "/home" + # fs_type: "nfs" + # mnt_opts: "defaults,nofail,_netdev" + # dump_freq: "0" + # fsck_pass: "0" # roles: ["slurm_control_node", "slurm_node", "login_node"] # hostnames: [] # groups: [] From cc352a6fc10d44715771ca6d7cc8426ee666c00d Mon Sep 17 00:00:00 2001 From: Jagadeesh N V Date: Fri, 20 Mar 2026 16:51:16 +0530 Subject: [PATCH 06/10] storage_config updates with proper vast powervault examples --- .../schema/storage_config.json | 149 +++------- input/storage_config.yml | 256 ++++++++++++------ 2 files changed, 214 insertions(+), 191 deletions(-) diff --git a/common/library/module_utils/input_validation/schema/storage_config.json b/common/library/module_utils/input_validation/schema/storage_config.json index 4bb924c608..bed03ea250 100644 --- a/common/library/module_utils/input_validation/schema/storage_config.json +++ b/common/library/module_utils/input_validation/schema/storage_config.json @@ -135,35 +135,21 @@ }, "uniqueItems": true }, - "hostnames": { - "type": "array", - "description": "List of specific hostnames to apply this mount to", - "items": { - "type": "string", - "pattern": "^[a-zA-Z0-9.-]+$" - }, - "uniqueItems": true - }, - "groups": { - "type": "array", - "description": "List of node groups to apply this mount to", - "items": { - "type": "string", - "pattern": "^[a-zA-Z0-9_-]+$" - }, - "uniqueItems": true + "mount_default_field": { + "type": "string", + "description": "Name of the default profile to use for missing optional fields (references mount_default_fields.name)", + "pattern": "^[a-zA-Z0-9_-]+$" } }, - "required": ["name", "fs_spec", "fs_file"], + "required": ["name", "source", "mount_point", "roles"], "anyOf": [ { - "required": ["roles"] + "required": ["mount_default_field"], + "description": "Must have mount_default_field to reference a default profile" }, { - "required": ["hostnames"] - }, - { - "required": ["groups"] + "required": ["mnt_opts"], + "description": "Must have mnt_opts explicitly specified" } ], "additionalProperties": false @@ -171,68 +157,36 @@ }, "mount_default_fields": { "type": "object", - "description": "Default mount configuration for any mount entry with less than 6 options provided", - "properties": { - "fs_type": { - "type": "string", - "description": "Filesystem type (e.g., ext4, xfs, nfs, cifs, auto)", - "enum": ["auto", "ext2", "ext3", "ext4", "xfs", "btrfs", "nfs", "nfs4", "cifs", "tmpfs", "cephfs", "vfat", "ntfs"] - }, - "mnt_opts": { - "type": "string", - "description": "Mount options (e.g., defaults,noexec,nofail)", - "pattern": "^[a-zA-Z0-9,=._-]+$" - }, - "dump_freq": { - "type": "string", - "description": "Dump frequency (usually 0)", - "pattern": "^[0-2]$" - }, - "fsck_pass": { - "type": "string", - "description": "Fsck pass number (usually 0 or 2)", - "pattern": "^[0-9]$" - }, - "roles": { - "type": "array", - "description": "List of node roles to apply these defaults to", - "items": { - "type": "string", - "pattern": "^[a-zA-Z0-9_-]+$" - }, - "uniqueItems": true - }, - "hostnames": { - "type": "array", - "description": "List of specific hostnames to apply these defaults to", - "items": { - "type": "string", - "pattern": "^[a-zA-Z0-9.-]+$" - }, - "uniqueItems": true - }, - "groups": { - "type": "array", - "description": "List of node groups to apply these defaults to", - "items": { - "type": "string", - "pattern": "^[a-zA-Z0-9_-]+$" + "description": "Named default mount configurations (key = profile name, value = profile settings)", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "type": "object", + "properties": { + "fs_type": { + "type": "string", + "description": "Default filesystem type (e.g., ext4, xfs, nfs, cifs, auto)", + "enum": ["auto", "ext2", "ext3", "ext4", "xfs", "btrfs", "nfs", "nfs4", "cifs", "tmpfs", "cephfs", "vfat", "ntfs", "none"] + }, + "mnt_opts": { + "type": "string", + "description": "Default mount options (e.g., defaults,noexec,nofail)", + "pattern": "^[a-zA-Z0-9,=._\\\\-]+$" + }, + "dump_freq": { + "type": "string", + "description": "Default dump frequency (usually 0)", + "pattern": "^[0-2]$" + }, + "fsck_pass": { + "type": "string", + "description": "Default fsck pass number (usually 0 or 2)", + "pattern": "^[0-9]$" + } }, - "uniqueItems": true + "required": ["fs_type", "mnt_opts", "dump_freq", "fsck_pass"], + "additionalProperties": false } }, - "required": ["fs_type", "mnt_opts", "dump_freq", "fsck_pass"], - "anyOf": [ - { - "required": ["roles"] - }, - { - "required": ["hostnames"] - }, - { - "required": ["groups"] - } - ], "additionalProperties": false }, "swap": { @@ -271,38 +225,9 @@ "pattern": "^[a-zA-Z0-9_-]+$" }, "uniqueItems": true - }, - "hostnames": { - "type": "array", - "description": "List of specific hostnames to apply this swap to", - "items": { - "type": "string", - "pattern": "^[a-zA-Z0-9.-]+$" - }, - "uniqueItems": true - }, - "groups": { - "type": "array", - "description": "List of node groups to apply this swap to", - "items": { - "type": "string", - "pattern": "^[a-zA-Z0-9_-]+$" - }, - "uniqueItems": true } }, - "required": ["name", "filename", "size"], - "anyOf": [ - { - "required": ["roles"] - }, - { - "required": ["hostnames"] - }, - { - "required": ["groups"] - } - ], + "required": ["name", "filename", "size", "roles"], "additionalProperties": false } } diff --git a/input/storage_config.yml b/input/storage_config.yml index 8f387f8d40..295fc6a0dc 100644 --- a/input/storage_config.yml +++ b/input/storage_config.yml @@ -79,101 +79,202 @@ nfs_client_params: # This configuration will be used to generate cloud-init compatible mount configurations # Each mount entry contains the following fields (matching /etc/fstab format): -# - name: Unique identifier for this mount entry (e.g., "data_mount", "nfs_home", "swap_storage"). Required -# - source: Device name (e.g., /dev/sdc, ephemeral0, UUID=xxx, or network path like 192.168.1.100:/export/share) -# - mount_point: Mount point path (e.g., /mnt, /opt/data) -# - fs_type: Filesystem type (e.g., ext4, xfs, nfs, cifs, auto), supported by mount module -# - nfs, tmpfs, cifs, cephfs, btrfs, ext2, ext3, ext4, xfs, etc. +# - name: Unique identifier for this mount entry (e.g., "iscsi_slurm_persist", "nfs_slurm_home"). Required +# - source: Device name (e.g., /dev/sdc, UUID=xxx, or network path like 192.168.1.100:/export/share). Required +# - mount_point: Mount point path (e.g., /mnt, /opt/data). Required +# - fs_type: Filesystem type (e.g., ext4, xfs, nfs, cifs, auto). Optional +# - Supported: nfs, nfs4, cifs, tmpfs, cephfs, btrfs, ext2, ext3, ext4, xfs, none, etc. # - For network filesystems (nfs, cifs, etc.), source should be the server path (e.g., 192.168.1.100:/export/share) -# - mnt_opts: Mount options (e.g., defaults,noexec,nofail). Optional, defaults to "defaults,nofail,x-systemd.after=cloud-init-network.service" -# - dump_freq: Dump frequency (usually "0"). Optional, defaults to "0" -# - fsck_pass: Fsck pass number (usually "0" or "2"). Optional, defaults to "2" -# - roles: List of node roles to apply this mount to (e.g., ["slurm_control_node", "slurm_node", "login_node"]). Optional -# - hostnames: List of specific hostnames to apply this mount to (e.g., ["node01", "node02"]). Optional -# - groups: List of node groups to apply this mount to (e.g., ["grp1", "grp2"]). Optional -# Note: If roles, hostnames, and groups are all empty or not specified, the mount will be applied to all nodes - -# mount_default_fields: -# - fields: Default mount configuration for any mount entry with less than 6 options provided. -# When specified, 6 items are required and represent /etc/fstab entries. -# Default: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] -# - roles: List of node roles to apply this default to (e.g., ["slurm_node", "kube_node"]). Optional -# Example: ["slurm_node", "kube_node"] -# - hostnames: List of specific hostnames to apply this default to (e.g., ["node01", "node02"]). Optional -# Example: ["node01", "node02"] -# - groups: List of node groups to apply this default to (e.g., ["grp1", "grp2"]). Optional -# Example: ["grp1", "grp2"] +# - If specified, takes PRIORITY over mount_default_field +# - mnt_opts: Mount options (e.g., defaults,noexec,nofail). Optional +# - If specified, takes PRIORITY over mount_default_field +# - dump_freq: Dump frequency (usually "0"). Optional +# - If specified, takes PRIORITY over mount_default_field +# - fsck_pass: Fsck pass number (usually "0" or "2"). Optional +# - If specified, takes PRIORITY over mount_default_field +# - mount_default_field: Name of the default profile to use (references mount_default_fields.name). Optional +# - Used ONLY for fields not explicitly specified in the mount entry +# - If not specified, system will auto-select based on fs_type or use global defaults +# - roles: List of node roles to apply this mount to (e.g., ["slurm_control_node", "slurm_node"]). Required +# +# PRIORITY ORDER for field resolution: +# 1. Explicit value in mount entry (HIGHEST PRIORITY) +# 2. Value from mount_default_field profile (if specified) +# 3. Auto-selected profile based on fs_type +# 4. Global fallback profile +# 5. Hardcoded system defaults (LOWEST PRIORITY) + +# mount_default_fields: Mapping of named default profiles (key = profile name) +# Each profile (value) contains: +# - fs_type: Default filesystem type (e.g., "auto", "xfs", "nfs"). Required +# - mnt_opts: Default mount options (e.g., "defaults,nofail,_netdev"). Required +# - dump_freq: Default dump frequency (usually "0"). Required +# - fsck_pass: Default fsck pass number (usually "0" or "2"). Required +# Note: Profiles are templates that can be referenced by any mount entry +# Note: Profile names must match pattern ^[a-zA-Z0-9_-]+$ (alphanumeric, underscore, hyphen) # swap: Swap file configuration (list of swap configurations) # Each swap entry contains the following fields: # - name: Unique identifier for this swap entry (e.g., "compute_swap", "slurm_swap"). Required -# - filename: Path to the swap file to create (e.g., /swapfile) -# - size: Size in bytes, 'auto', or human-readable format (e.g., "2G", "512M") +# - filename: Path to the swap file to create (e.g., /swapfile). Required +# - size: Size in bytes, 'auto', or human-readable format (e.g., "2G", "512M"). Required # Units: B, K, M, G, T (Note: 1K = 1024B) -# - maxsize: Maximum size in bytes or human-readable format (used with size: auto) -# - roles: List of node roles to apply this swap to (e.g., ["slurm_node", "kube_node"]). Optional -# - groups: List of node groups to apply this swap to (e.g., ["grp1", "grp2"]). Optional -# - hostnames: List of specific hostnames to apply this swap to (e.g., ["node01", "node02"]). Optional -# Note: If roles, hostnames, and groups are all empty or not specified, the swap will be applied to all nodes +# - maxsize: Maximum size in bytes or human-readable format (used with size: auto). Optional +# - roles: List of node roles to apply this swap to (e.g., ["slurm_node"]). Required # Example configuration: # mounts: -# - name: "node_storage_mount" -# source: "/dev/sdc" -# mount_point: "/opt/data" -# fs_type: "ext4" -# mnt_opts: "defaults,nofail" -# dump_freq: "0" -# fsck_pass: "2" -# roles: ["slurm_node"] -# hostnames: [] -# groups: ["grp1"] -# - name: "ephemeral_mount" -# source: "ephemeral0" -# mount_point: "/mnt" -# fs_type: "auto" -# mnt_opts: "defaults,noexec" -# roles: [] -# hostnames: ["node01", "node02"] -# groups: [] -# - name: "nfs_slurm" -# source: "192.168.1.100:/export/share" -# mount_point: "/mnt/nfs" -# fs_type: "nfs" -# mnt_opts: "defaults,nofail,_netdev" +# # Example 1: PowerVault iSCSI storage - Fully specified for Slurm persistent storage +# - name: "powervault_slurm_persist" +# source: "/dev/mapper/mpatha1" # PowerVault multipath device partition +# mount_point: "/mnt/slurm-persist" +# fs_type: "xfs" +# mnt_opts: "defaults,_netdev,noatime,x-systemd.requires=iscsi.service" # dump_freq: "0" # fsck_pass: "0" +# roles: ["slurm_control_node"] +# # All fields explicit - mount_default_field not needed +# +# # Example 2: VAST NFS storage - Home directories using default profile +# - name: "vast_home" +# source: "192.168.1.100:/home" # VAST NFS export +# mount_point: "/home" +# mount_default_field: "default" +# # fs_type, mnt_opts, dump_freq, fsck_pass ALL from default profile # roles: ["slurm_control_node", "slurm_node", "login_node"] -# hostnames: [] -# groups: [] -# - name: "cifs_k8s" -# source: "//server/share" -# mount_point: "/mnt/cifs" -# fs_type: "cifs" -# mnt_opts: "credentials=/root/.smbcreds,nofail" +# +# # Example 3: VAST NFS storage - Shared applications with explicit fs_type +# - name: "vast_apps" +# source: "192.168.1.100:/apps" # VAST NFS export +# mount_point: "/opt/apps" +# fs_type: "nfs4" # ← EXPLICIT (takes priority over profile) +# mount_default_field: "network_storage" +# # mnt_opts, dump_freq, fsck_pass from network_storage profile +# # fs_type from network_storage is IGNORED (explicit wins) +# roles: ["slurm_control_node", "slurm_node"] +# +# # Example 4: VAST NFS storage - Read-only data with custom mount options +# - name: "vast_data_ro" +# source: "192.168.1.100:/datasets" # VAST NFS export +# mount_point: "/data" +# fs_type: "nfs4" # ← EXPLICIT +# mnt_opts: "defaults,nofail,_netdev,ro,noatime" # ← EXPLICIT (read-only) +# mount_default_field: "network_storage" +# # dump_freq, fsck_pass from network_storage profile +# # fs_type and mnt_opts from network_storage are IGNORED (explicit wins) +# roles: ["slurm_node"] +# +# # Example 5: PowerVault bind mount - MySQL data directory +# - name: "powervault_mysql_bind" +# source: "/mnt/slurm-persist/mysql" +# mount_point: "/var/lib/mysql" +# mount_default_field: "bind_mounts" +# # fs_type, mnt_opts, dump_freq, fsck_pass ALL from bind_mounts profile +# roles: ["slurm_control_node"] +# +# # Example 6: VAST NFS storage - Scratch space with high-performance options +# - name: "vast_scratch" +# source: "192.168.1.100:/scratch" # VAST NFS export +# mount_point: "/scratch" +# fs_type: "nfs4" +# mnt_opts: "defaults,nofail,_netdev,noatime,nodiratime,rsize=1048576,wsize=1048576" # dump_freq: "0" # fsck_pass: "0" -# roles: ['kube_control_plane', 'kube_node'] -# hostnames: [] -# groups: ["grp2"] +# roles: ["slurm_node"] mounts: [] - # - name: "nfs_slurm_home" - # source: "172.16.107.168:/mnt/share/omnia" + # Example 1: VAST NFS storage - Home directories using default NFS profile + # - name: "vast_home" + # source: "192.168.1.100:/home" # mount_point: "/home" - # fs_type: "nfs" - # mnt_opts: "defaults,nofail,_netdev" + # mount_default_field: "default" + # # fs_type (nfs), mnt_opts, dump_freq, fsck_pass will be filled from default profile + # roles: ["slurm_control_node", "slurm_node", "login_node"] + # + # Example 2: PowerVault iSCSI storage - Fully specified persistent storage + # - name: "powervault_slurm_persist" + # source: "/dev/mapper/mpatha1" #TODO: how to derive this? + # mount_point: "/mnt/slurm-persist" + # fs_type: "xfs" + # mnt_opts: "defaults,_netdev,noatime,x-systemd.requires=iscsi.service" # dump_freq: "0" # fsck_pass: "0" - # roles: ["slurm_control_node", "slurm_node", "login_node"] - # hostnames: [] - # groups: [] + # roles: ["slurm_control_node"] + # + # Example 3: VAST NFS storage - High-performance scratch with custom options + # - name: "vast_scratch" + # source: "192.168.1.100:/scratch" + # mount_point: "/scratch" + # fs_type: "nfs4" + # mnt_opts: "defaults,nofail,_netdev,noatime,nodiratime,rsize=1048576,wsize=1048576" + # dump_freq: "0" + # fsck_pass: "0" + # roles: ["slurm_node"] +# Named default profiles for mount configurations mount_default_fields: - fields: ["auto", "defaults,nofail,x-systemd.after=cloud-init-network.service", "0", "2"] - roles: [] - hostnames: [] - groups: [] + # Default NFS mount configuration - standard NFS defaults + default: + fs_type: "nfs" + mnt_opts: "defaults,nofail,_netdev,x-systemd.after=cloud-init-network.service" + dump_freq: "0" + fsck_pass: "0" + + # VAST NFS storage - Standard configuration for VAST Data NFS exports + vast_nfs: + fs_type: "nfs4" + mnt_opts: "defaults,nofail,_netdev,noatime,x-systemd.after=cloud-init-network.service" + dump_freq: "0" + fsck_pass: "0" + + # VAST NFS storage - High-performance configuration with large buffers + vast_nfs_performance: + fs_type: "nfs4" + mnt_opts: "defaults,nofail,_netdev,noatime,nodiratime,rsize=1048576,wsize=1048576" + dump_freq: "0" + fsck_pass: "0" + + # PowerVault iSCSI storage - Block device with XFS filesystem + powervault_iscsi: + fs_type: "xfs" + mnt_opts: "defaults,_netdev,noatime,x-systemd.requires=iscsi.service" + dump_freq: "0" + fsck_pass: "0" + + # Network storage defaults (generic NFS, CIFS, etc.) + network_storage: + fs_type: "auto" + mnt_opts: "defaults,nofail,_netdev,x-systemd.after=cloud-init-network.service" + dump_freq: "0" + fsck_pass: "0" + + # Local storage defaults (ext4, xfs, etc.) + local_storage: + fs_type: "auto" + mnt_opts: "defaults,nofail,noatime" + dump_freq: "0" + fsck_pass: "2" + + # Bind mount defaults + bind_mounts: + fs_type: "none" + mnt_opts: "bind" + dump_freq: "0" + fsck_pass: "0" + + # High-performance scratch storage + scratch_storage: + fs_type: "xfs" + mnt_opts: "defaults,nofail,noatime,nodiratime,largeio,inode64" + dump_freq: "0" + fsck_pass: "2" + + # Global fallback defaults + global: + fs_type: "auto" + mnt_opts: "defaults,nofail,x-systemd.after=cloud-init-network.service" + dump_freq: "0" + fsck_pass: "2" # Example swap configuration: # swap: @@ -182,20 +283,17 @@ mount_default_fields: # size: "4G" # maxsize: "8G" # roles: ["slurm_node"] -# groups: [] +# # - name: "k8s_swap" # filename: "/swapfile" # size: "2G" # maxsize: "4G" # roles: ["kube_node"] -# groups: ["grp2"] swap: [] # - name: "compute_swap" # filename: "/swapfile" # size: "2G" # maxsize: "4G" - # roles: [] - # groups: [] - # hostnames: [] + # roles: ["slurm_node"] From 04dd2d0d78cdbeef2d53806cd2cf626796d03cb8 Mon Sep 17 00:00:00 2001 From: Jagadeesh N V Date: Fri, 20 Mar 2026 17:01:49 +0530 Subject: [PATCH 07/10] Enhanced examples for storage mounts --- STORAGE_DESIGN.md | 1008 ++++++++++++++++++++++++++++++++++++++ input/storage_config.yml | 99 ++-- 2 files changed, 1054 insertions(+), 53 deletions(-) create mode 100644 STORAGE_DESIGN.md diff --git a/STORAGE_DESIGN.md b/STORAGE_DESIGN.md new file mode 100644 index 0000000000..68f5d9436d --- /dev/null +++ b/STORAGE_DESIGN.md @@ -0,0 +1,1008 @@ +# Storage Configuration Design + +**Version:** 1.0 +**Date:** 2026-03-20 +**Status:** Design Complete + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Architecture](#architecture) +3. [Configuration Schema](#configuration-schema) +4. [Mount Default Fields (Profiles)](#mount-default-fields-profiles) +5. [Storage Technologies](#storage-technologies) +6. [Usage Examples](#usage-examples) +7. [Priority Resolution](#priority-resolution) +8. [Best Practices](#best-practices) +9. [Validation Rules](#validation-rules) + +--- + +## Overview + +This design provides a flexible, profile-based mount configuration system for Dell storage solutions in HPC environments. It supports: + +- **VAST NFS Storage** - High-performance NFS storage for shared filesystems +- **PowerVault iSCSI Storage** - Block storage for persistent data and databases +- **Generic Network Storage** - NFS, CIFS, and other network filesystems +- **Local Storage** - Direct-attached storage and local disks + +### Key Features + +- ✅ **Profile-based configuration** - Reusable templates for common mount patterns +- ✅ **Priority-based resolution** - Explicit values override profile defaults +- ✅ **Vendor-specific optimizations** - VAST and PowerVault tuned profiles +- ✅ **Role-based targeting** - Mount configurations per node role +- ✅ **Validation enforcement** - Schema validation ensures correct configuration + +--- + +## Architecture + +### Configuration Flow + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ storage_config.yml │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ mount_default_fields (Profiles) │ │ +│ │ ├─ vast_nfs │ │ +│ │ ├─ vast_nfs_performance │ │ +│ │ ├─ powervault_iscsi │ │ +│ │ ├─ network_storage │ │ +│ │ └─ bind_mounts │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ mounts (Mount Entries) │ │ +│ │ ├─ vast_home (uses vast_nfs profile) │ │ +│ │ ├─ powervault_persist (uses powervault_iscsi) │ │ +│ │ └─ powervault_mysql_bind (uses bind_mounts) │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ powervault_config (Optional) │ │ +│ │ ├─ ip: [controller IPs] │ │ +│ │ ├─ iscsi_initiator: IQN │ │ +│ │ └─ volume_id: WWN │ │ +│ └────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + ▼ + ┌──────────────────────────────────────┐ + │ Schema Validation │ + │ (storage_config.json) │ + └──────────────────────────────────────┘ + ▼ + ┌──────────────────────────────────────┐ + │ Cloud-Init Generation │ + │ (Jinja2 Templates) │ + └──────────────────────────────────────┘ + ▼ + ┌──────────────────────────────────────┐ + │ Node Provisioning │ + │ (Per Role) │ + └──────────────────────────────────────┘ +``` + +--- + +## Configuration Schema + +### File Structure + +```yaml +# storage_config.yml + +# PowerVault iSCSI configuration (optional) +powervault_config: + ip: [list of controller IPs] + port: 3260 + iscsi_initiator: "iqn.2025-01.com.dell:hostname" + volume_id: "00c0ff4343f1f1f1001c8c4e6901000000" + +# Mount default profiles (templates) +mount_default_fields: + profile_name: + fs_type: "filesystem_type" + mnt_opts: "mount_options" + dump_freq: "0" + fsck_pass: "0" + +# Mount entries +mounts: + - name: "unique_mount_name" + source: "device_or_network_path" + mount_point: "/mount/path" + mount_default_field: "profile_name" # Optional + fs_type: "filesystem_type" # Optional (overrides profile) + mnt_opts: "mount_options" # Optional (overrides profile) + dump_freq: "0" # Optional (overrides profile) + fsck_pass: "0" # Optional (overrides profile) + roles: ["role1", "role2"] # Required + +# Swap configuration (optional) +swap: + - name: "swap_name" + filename: "/swapfile" + size: "4G" + roles: ["role1"] +``` + +--- + +## Mount Default Fields (Profiles) + +Profiles are reusable templates that provide default values for mount configurations. They define the "HOW to mount" (technical settings), while mount entries define the "WHAT/WHERE/WHOM" (targeting and specifics). + +### Profile Structure + +```yaml +mount_default_fields: + profile_name: + fs_type: "filesystem_type" # Required + mnt_opts: "mount_options" # Required + dump_freq: "0" # Required (usually "0") + fsck_pass: "0" # Required (usually "0" or "2") +``` + +### Standard Profiles + +#### 1. `default` - Standard NFS Defaults + +```yaml +default: + fs_type: "nfs" + mnt_opts: "defaults,nofail,_netdev,x-systemd.after=cloud-init-network.service" + dump_freq: "0" + fsck_pass: "0" +``` + +**Use Case:** Generic NFS mounts with standard options + +--- + +#### 2. `vast_nfs` - VAST NFS Standard Configuration + +```yaml +vast_nfs: + fs_type: "nfs4" + mnt_opts: "defaults,nofail,_netdev,noatime,x-systemd.after=cloud-init-network.service" + dump_freq: "0" + fsck_pass: "0" +``` + +**Use Case:** VAST Data NFS exports with standard performance +**Features:** +- NFSv4 protocol +- `noatime` for improved performance +- Network dependency handling + +--- + +#### 3. `vast_nfs_performance` - VAST NFS High-Performance + +```yaml +vast_nfs_performance: + fs_type: "nfs4" + mnt_opts: "defaults,nofail,_netdev,noatime,nodiratime,rsize=1048576,wsize=1048576" + dump_freq: "0" + fsck_pass: "0" +``` + +**Use Case:** VAST Data NFS for high-throughput workloads (scratch, datasets) +**Features:** +- NFSv4 protocol +- **1MB read/write buffers** (`rsize=1048576,wsize=1048576`) +- `noatime` and `nodiratime` for maximum performance +- Optimized for large sequential I/O + +--- + +#### 4. `powervault_iscsi` - PowerVault iSCSI Block Storage + +```yaml +powervault_iscsi: + fs_type: "xfs" + mnt_opts: "defaults,_netdev,noatime,x-systemd.requires=iscsi.service" + dump_freq: "0" + fsck_pass: "0" +``` + +**Use Case:** PowerVault iSCSI persistent storage +**Features:** +- XFS filesystem (high performance, scalability) +- Requires iSCSI service to be running +- `noatime` for improved performance +- Network device handling + +**Note:** Matches `setup_iscsi_storage.sh` default configuration + +--- + +#### 5. `network_storage` - Generic Network Storage + +```yaml +network_storage: + fs_type: "auto" + mnt_opts: "defaults,nofail,_netdev,x-systemd.after=cloud-init-network.service" + dump_freq: "0" + fsck_pass: "0" +``` + +**Use Case:** Generic network filesystems (NFS, CIFS, etc.) + +--- + +#### 6. `local_storage` - Local Disk Storage + +```yaml +local_storage: + fs_type: "auto" + mnt_opts: "defaults,nofail,noatime" + dump_freq: "0" + fsck_pass: "2" +``` + +**Use Case:** Local disks (ext4, xfs, etc.) +**Note:** `fsck_pass: "2"` enables filesystem check + +--- + +#### 7. `bind_mounts` - Bind Mounts + +```yaml +bind_mounts: + fs_type: "none" + mnt_opts: "bind" + dump_freq: "0" + fsck_pass: "0" +``` + +**Use Case:** Bind mounts (e.g., `/mnt/slurm-persist/mysql` → `/var/lib/mysql`) + +--- + +#### 8. `scratch_storage` - High-Performance Scratch + +```yaml +scratch_storage: + fs_type: "xfs" + mnt_opts: "defaults,nofail,noatime,nodiratime,largeio,inode64" + dump_freq: "0" + fsck_pass: "2" +``` + +**Use Case:** Local high-performance scratch storage + +--- + +#### 9. `global` - Global Fallback + +```yaml +global: + fs_type: "auto" + mnt_opts: "defaults,nofail,x-systemd.after=cloud-init-network.service" + dump_freq: "0" + fsck_pass: "2" +``` + +**Use Case:** Fallback when no specific profile matches + +--- + +## Storage Technologies + +### VAST NFS Storage + +**Overview:** VAST Data provides high-performance, scale-out NFS storage optimized for HPC workloads. + +#### Configuration Format + +```yaml +mounts: + - name: "vast_home" + source: "192.168.1.100:/home" # VAST NFS export + mount_point: "/home" + mount_default_field: "vast_nfs" + roles: ["slurm_control_node", "slurm_node", "login_node"] +``` + +#### Source Format + +- **Pattern:** `:` +- **Example:** `192.168.1.100:/home` + +#### Recommended Profiles + +| Use Case | Profile | Reason | +|----------|---------|--------| +| Home directories | `vast_nfs` | Standard performance, shared access | +| Shared applications | `vast_nfs` | Standard performance, read-heavy | +| Datasets (read-only) | `vast_nfs` | Standard performance, read-only | +| Scratch space | `vast_nfs_performance` | High throughput, large I/O | +| Checkpoints | `vast_nfs_performance` | High throughput, write-heavy | + +#### Performance Tuning + +**Standard Configuration (`vast_nfs`):** +- Default NFS buffer sizes +- Suitable for most workloads +- Lower memory overhead + +**High-Performance Configuration (`vast_nfs_performance`):** +- 1MB read/write buffers +- Optimized for large sequential I/O +- Higher memory usage +- Best for scratch, checkpoints, large datasets + +--- + +### PowerVault iSCSI Storage + +**Overview:** Dell PowerVault provides block-level iSCSI storage for persistent data and databases. + +#### Configuration Format + +```yaml +# 1. Configure PowerVault connection +powervault_config: + ip: + - 172.1.2.3 + - 172.1.2.4 + port: 3260 + iscsi_initiator: "iqn.2025-01.com.dell:scontrol-node" + volume_id: "00c0ff4343f1f1f1001c8c4e6901000000" + +# 2. Mount PowerVault persistent storage +mounts: + - name: "powervault_slurm_persist" + source: "UUID=" + mount_point: "/mnt/slurm-persist" + mount_default_field: "powervault_iscsi" + roles: ["slurm_control_node"] + + # 3. Bind mount for MySQL + - name: "powervault_mysql_bind" + source: "/mnt/slurm-persist/mysql" + mount_point: "/var/lib/mysql" + mount_default_field: "bind_mounts" + roles: ["slurm_control_node"] + + # 4. Bind mount for Slurm spool + - name: "powervault_spool_bind" + source: "/mnt/slurm-persist/spool" + mount_point: "/var/spool" + mount_default_field: "bind_mounts" + roles: ["slurm_control_node"] +``` + +#### Source Format + +- **Pattern:** `UUID=` or `/dev/mapper/` +- **Example:** `UUID=12345678-1234-1234-1234-123456789abc` +- **Note:** UUID is preferred for persistence + +#### Setup Script Integration + +The `setup_iscsi_storage.sh` script automatically: + +1. Discovers iSCSI targets from controller IPs +2. Logs in to all discovered targets +3. Configures multipath for redundancy +4. Selects the correct volume using `volume_id` +5. Creates GPT partition table +6. Formats partition with XFS +7. Mounts to `/mnt/slurm-persist` using UUID +8. Creates subdirectories: `mysql/`, `spool/` +9. Sets up bind mounts in `/etc/fstab` + +#### Recommended Workflow + +``` +PowerVault Setup + ↓ +/mnt/slurm-persist (XFS on /dev/mapper/mpatha1) + ↓ + ├─ mysql/ → bind mount to /var/lib/mysql + └─ spool/ → bind mount to /var/spool +``` + +#### Best Practices + +- ✅ Use UUID for source (more reliable than device paths) +- ✅ Use XFS filesystem (default in setup script) +- ✅ Create subdirectories for different services +- ✅ Use bind mounts to map subdirectories to system paths +- ✅ Ensure iSCSI service dependency in mount options + +--- + +## Usage Examples + +### Example 1: VAST Home Directories + +```yaml +mounts: + - name: "vast_home" + source: "192.168.1.100:/home" + mount_point: "/home" + mount_default_field: "vast_nfs" + roles: ["slurm_control_node", "slurm_node", "login_node"] +``` + +**Result:** +- Filesystem: NFSv4 +- Mount options: `defaults,nofail,_netdev,noatime,x-systemd.after=cloud-init-network.service` +- Applied to: All Slurm nodes + +--- + +### Example 2: VAST High-Performance Scratch + +```yaml +mounts: + - name: "vast_scratch" + source: "192.168.1.100:/scratch" + mount_point: "/scratch" + mount_default_field: "vast_nfs_performance" + roles: ["slurm_node"] +``` + +**Result:** +- Filesystem: NFSv4 +- Mount options: `defaults,nofail,_netdev,noatime,nodiratime,rsize=1048576,wsize=1048576` +- Applied to: Compute nodes only +- Performance: 1MB read/write buffers + +--- + +### Example 3: PowerVault Persistent Storage + +```yaml +powervault_config: + ip: + - 172.1.2.3 + - 172.1.2.4 + port: 3260 + iscsi_initiator: "iqn.2025-01.com.dell:scontrol-node" + volume_id: "00c0ff4343f1f1f1001c8c4e6901000000" + +mounts: + - name: "powervault_slurm_persist" + source: "UUID=" + mount_point: "/mnt/slurm-persist" + mount_default_field: "powervault_iscsi" + roles: ["slurm_control_node"] +``` + +**Result:** +- Filesystem: XFS +- Mount options: `defaults,_netdev,noatime,x-systemd.requires=iscsi.service` +- Applied to: Control node only +- Setup: Automated via `setup_iscsi_storage.sh` + +--- + +### Example 4: PowerVault Bind Mounts + +```yaml +mounts: + # Main persistent storage + - name: "powervault_slurm_persist" + source: "UUID=" + mount_point: "/mnt/slurm-persist" + mount_default_field: "powervault_iscsi" + roles: ["slurm_control_node"] + + # MySQL data directory + - name: "powervault_mysql_bind" + source: "/mnt/slurm-persist/mysql" + mount_point: "/var/lib/mysql" + mount_default_field: "bind_mounts" + roles: ["slurm_control_node"] + + # Slurm spool directory + - name: "powervault_spool_bind" + source: "/mnt/slurm-persist/spool" + mount_point: "/var/spool" + mount_default_field: "bind_mounts" + roles: ["slurm_control_node"] +``` + +**Result:** +- Main mount: `/mnt/slurm-persist` (XFS on iSCSI) +- Bind mount 1: `/var/lib/mysql` → `/mnt/slurm-persist/mysql` +- Bind mount 2: `/var/spool` → `/mnt/slurm-persist/spool` +- All on control node only + +--- + +### Example 5: Explicit Override + +```yaml +mounts: + - name: "vast_apps_custom" + source: "192.168.1.100:/apps" + mount_point: "/opt/apps" + fs_type: "nfs4" # ← EXPLICIT + mnt_opts: "defaults,nofail,_netdev,ro" # ← EXPLICIT (read-only) + mount_default_field: "network_storage" + # Only dump_freq and fsck_pass from profile + roles: ["slurm_control_node", "slurm_node"] +``` + +**Result:** +- Filesystem: NFSv4 (explicit, not from profile) +- Mount options: `defaults,nofail,_netdev,ro` (explicit, read-only) +- Dump frequency: `0` (from profile) +- Fsck pass: `0` (from profile) + +--- + +## Priority Resolution + +When a mount entry references a profile, field values are resolved using this priority order: + +### Priority Order (Highest to Lowest) + +``` +1. Explicit value in mount entry ← HIGHEST PRIORITY +2. Value from mount_default_field profile +3. Auto-selected profile based on fs_type +4. Global fallback profile +5. Hardcoded system defaults ← LOWEST PRIORITY +``` + +### Resolution Examples + +#### Example 1: Full Profile Usage + +```yaml +mount_default_fields: + vast_nfs: + fs_type: "nfs4" + mnt_opts: "defaults,nofail,_netdev,noatime" + dump_freq: "0" + fsck_pass: "0" + +mounts: + - name: "vast_home" + source: "192.168.1.100:/home" + mount_point: "/home" + mount_default_field: "vast_nfs" + roles: ["slurm_node"] +``` + +**Resolution:** +- `fs_type`: `"nfs4"` ← from `vast_nfs` profile +- `mnt_opts`: `"defaults,nofail,_netdev,noatime"` ← from `vast_nfs` profile +- `dump_freq`: `"0"` ← from `vast_nfs` profile +- `fsck_pass`: `"0"` ← from `vast_nfs` profile + +--- + +#### Example 2: Partial Override + +```yaml +mount_default_fields: + vast_nfs: + fs_type: "nfs4" + mnt_opts: "defaults,nofail,_netdev,noatime" + dump_freq: "0" + fsck_pass: "0" + +mounts: + - name: "vast_apps" + source: "192.168.1.100:/apps" + mount_point: "/opt/apps" + fs_type: "nfs4" # ← EXPLICIT + mnt_opts: "defaults,nofail,_netdev,ro" # ← EXPLICIT + mount_default_field: "vast_nfs" + roles: ["slurm_node"] +``` + +**Resolution:** +- `fs_type`: `"nfs4"` ← **EXPLICIT (priority 1)** - profile value ignored +- `mnt_opts`: `"defaults,nofail,_netdev,ro"` ← **EXPLICIT (priority 1)** - profile value ignored +- `dump_freq`: `"0"` ← from `vast_nfs` profile (priority 2) +- `fsck_pass`: `"0"` ← from `vast_nfs` profile (priority 2) + +--- + +#### Example 3: No Profile Specified + +```yaml +mounts: + - name: "vast_data" + source: "192.168.1.100:/data" + mount_point: "/data" + fs_type: "nfs4" + mnt_opts: "defaults,nofail,_netdev" + dump_freq: "0" + fsck_pass: "0" + roles: ["slurm_node"] +``` + +**Resolution:** +- `fs_type`: `"nfs4"` ← EXPLICIT (priority 1) +- `mnt_opts`: `"defaults,nofail,_netdev"` ← EXPLICIT (priority 1) +- `dump_freq`: `"0"` ← EXPLICIT (priority 1) +- `fsck_pass`: `"0"` ← EXPLICIT (priority 1) +- No profile needed - all fields explicit + +--- + +## Best Practices + +### Profile Design + +✅ **DO:** +- Create profiles for common storage patterns +- Use descriptive profile names (`vast_nfs`, `powervault_iscsi`) +- Document profile purpose and use cases +- Keep profiles simple and focused + +❌ **DON'T:** +- Include roles in profiles (roles belong in mount entries) +- Create too many similar profiles +- Use generic names like `profile1`, `profile2` + +--- + +### Mount Configuration + +✅ **DO:** +- Use profiles for standard configurations +- Override specific fields when needed +- Use UUID for PowerVault sources +- Specify roles for each mount +- Use descriptive mount names + +❌ **DON'T:** +- Duplicate mount options across entries (use profiles) +- Mix explicit and profile values unnecessarily +- Use device paths for PowerVault (use UUID) +- Forget to specify roles + +--- + +### Storage Selection + +| Requirement | Recommended Storage | Profile | +|-------------|---------------------|---------| +| Shared home directories | VAST NFS | `vast_nfs` | +| Shared applications | VAST NFS | `vast_nfs` | +| High-throughput scratch | VAST NFS | `vast_nfs_performance` | +| Large datasets | VAST NFS | `vast_nfs_performance` | +| Persistent databases | PowerVault iSCSI | `powervault_iscsi` | +| Slurm state files | PowerVault iSCSI | `powervault_iscsi` | +| Local scratch | Local disk | `scratch_storage` | + +--- + +### Performance Optimization + +#### VAST NFS + +**Standard Workloads (`vast_nfs`):** +- Home directories +- Shared applications +- Small file I/O +- Metadata-heavy operations + +**High-Performance Workloads (`vast_nfs_performance`):** +- Scratch space +- Checkpointing +- Large sequential I/O +- Streaming data + +**Buffer Size Tuning:** +```yaml +# Standard: Default buffers (typically 32KB-128KB) +mnt_opts: "defaults,nofail,_netdev,noatime" + +# High-performance: 1MB buffers +mnt_opts: "defaults,nofail,_netdev,noatime,nodiratime,rsize=1048576,wsize=1048576" +``` + +#### PowerVault iSCSI + +**Filesystem Choice:** +- ✅ **XFS** (recommended) - High performance, scalability, large files +- ⚠️ **ext4** - Good compatibility, lower performance at scale + +**Mount Options:** +```yaml +# Recommended +mnt_opts: "defaults,_netdev,noatime,x-systemd.requires=iscsi.service" + +# Additional options for databases +mnt_opts: "defaults,_netdev,noatime,nobarrier,x-systemd.requires=iscsi.service" +``` + +--- + +## Validation Rules + +### Required Fields + +#### Mount Entry + +- ✅ `name` - Unique identifier +- ✅ `source` - Device or network path +- ✅ `mount_point` - Mount point path +- ✅ `roles` - List of target roles +- ✅ **At least one of:** + - `mount_default_field` (references a profile), OR + - `mnt_opts` (explicit mount options) + +#### Profile + +- ✅ `fs_type` - Filesystem type +- ✅ `mnt_opts` - Mount options +- ✅ `dump_freq` - Dump frequency +- ✅ `fsck_pass` - Fsck pass number + +--- + +### Field Validation + +#### `name` +- Pattern: `^[a-zA-Z0-9_-]+$` +- Length: 1-64 characters +- Must be unique across all mounts + +#### `source` +- Minimum length: 1 character +- Examples: + - `/dev/sda1` + - `UUID=12345678-1234-1234-1234-123456789abc` + - `192.168.1.100:/export/share` + +#### `mount_point` +- Pattern: `^/[a-zA-Z0-9/_.-]*$` +- Must start with `/` + +#### `fs_type` +- Allowed values: `auto`, `ext2`, `ext3`, `ext4`, `xfs`, `btrfs`, `nfs`, `nfs4`, `cifs`, `tmpfs`, `cephfs`, `vfat`, `ntfs`, `none` + +#### `mnt_opts` +- Pattern: `^[a-zA-Z0-9,=._-]+$` +- Examples: `defaults,nofail,_netdev` + +#### `dump_freq` +- Pattern: `^[0-2]$` +- Usually `0` (no dump) + +#### `fsck_pass` +- Pattern: `^[0-9]$` +- Common values: + - `0` - No fsck (network filesystems, bind mounts) + - `1` - Root filesystem + - `2` - Other local filesystems + +#### `roles` +- Array of strings +- Pattern per role: `^[a-zA-Z0-9_-]+$` +- Must be unique within array +- Examples: `slurm_control_node`, `slurm_node`, `login_node` + +--- + +### Profile Validation + +#### Profile Name (Key) +- Pattern: `^[a-zA-Z0-9_-]+$` +- Must be unique across all profiles +- Examples: `vast_nfs`, `powervault_iscsi`, `network_storage` + +#### Profile Structure +```json +{ + "type": "object", + "properties": { + "fs_type": { "type": "string", "enum": [...] }, + "mnt_opts": { "type": "string", "pattern": "^[a-zA-Z0-9,=._-]+$" }, + "dump_freq": { "type": "string", "pattern": "^[0-2]$" }, + "fsck_pass": { "type": "string", "pattern": "^[0-9]$" } + }, + "required": ["fs_type", "mnt_opts", "dump_freq", "fsck_pass"], + "additionalProperties": false +} +``` + +--- + +### Conditional Validation + +#### Mount Entry Must Have Profile OR Explicit Options + +```json +{ + "anyOf": [ + { "required": ["mount_default_field"] }, + { "required": ["mnt_opts"] } + ] +} +``` + +**Valid:** +```yaml +# Has mount_default_field +- name: "mount1" + source: "..." + mount_point: "..." + mount_default_field: "vast_nfs" + roles: [...] + +# Has mnt_opts +- name: "mount2" + source: "..." + mount_point: "..." + mnt_opts: "defaults,nofail" + roles: [...] + +# Has both (explicit wins) +- name: "mount3" + source: "..." + mount_point: "..." + mount_default_field: "vast_nfs" + mnt_opts: "defaults,nofail,ro" # Overrides profile + roles: [...] +``` + +**Invalid:** +```yaml +# Missing both mount_default_field and mnt_opts +- name: "mount_invalid" + source: "..." + mount_point: "..." + roles: [...] +``` + +--- + +## Quick Reference + +### Profile Selection Guide + +| Storage Type | Use Case | Profile | Key Features | +|--------------|----------|---------|--------------| +| **VAST** | Home directories | `vast_nfs` | NFSv4, standard perf | +| **VAST** | Shared apps | `vast_nfs` | NFSv4, standard perf | +| **VAST** | Scratch space | `vast_nfs_performance` | NFSv4, 1MB buffers | +| **VAST** | Large datasets | `vast_nfs_performance` | NFSv4, 1MB buffers | +| **PowerVault** | Persistent storage | `powervault_iscsi` | XFS, iSCSI service | +| **PowerVault** | Database bind | `bind_mounts` | Bind from persistent | +| **Generic** | Network FS | `network_storage` | Auto-detect FS | +| **Local** | Local disk | `local_storage` | Auto-detect FS | +| **Local** | Scratch | `scratch_storage` | XFS, optimized | + +--- + +### Common Mount Options + +| Option | Description | Use Case | +|--------|-------------|----------| +| `defaults` | Use default options | All mounts | +| `nofail` | Don't fail boot if mount fails | Network mounts | +| `_netdev` | Network device (wait for network) | Network mounts | +| `noatime` | Don't update access time | Performance | +| `nodiratime` | Don't update directory access time | Performance | +| `ro` | Read-only | Shared apps, datasets | +| `rw` | Read-write | Default | +| `bind` | Bind mount | Subdirectory mounts | +| `rsize=1048576` | 1MB read buffer | High-perf NFS | +| `wsize=1048576` | 1MB write buffer | High-perf NFS | +| `x-systemd.requires=iscsi.service` | Require iSCSI service | PowerVault | +| `x-systemd.after=cloud-init-network.service` | Wait for cloud-init network | Network mounts | + +--- + +### Filesystem Types + +| Type | Description | Use Case | +|------|-------------|----------| +| `nfs` | NFS version 3 | Legacy NFS | +| `nfs4` | NFS version 4 | Modern NFS (VAST) | +| `xfs` | XFS filesystem | PowerVault, local disks | +| `ext4` | ext4 filesystem | Local disks | +| `cifs` | SMB/CIFS | Windows shares | +| `none` | No filesystem | Bind mounts | +| `auto` | Auto-detect | Generic mounts | + +--- + +## Troubleshooting + +### Common Issues + +#### Issue: Mount fails with "mount.nfs: Connection timed out" + +**Cause:** Network not ready or VAST server unreachable + +**Solution:** +- Ensure `_netdev` and `x-systemd.after=cloud-init-network.service` in mount options +- Verify VAST server IP is correct and reachable +- Check firewall rules (NFS ports: 2049, 111) + +--- + +#### Issue: PowerVault mount fails with "No such device" + +**Cause:** iSCSI service not running or multipath device not ready + +**Solution:** +- Ensure `x-systemd.requires=iscsi.service` in mount options +- Verify `powervault_config` is correctly configured +- Check iSCSI discovery: `iscsiadm -m discovery -t sendtargets -p ` +- Check multipath devices: `multipath -ll` + +--- + +#### Issue: Bind mount fails with "mount point does not exist" + +**Cause:** Source directory doesn't exist + +**Solution:** +- Ensure parent mount is mounted first +- Create source directory: `mkdir -p /mnt/slurm-persist/mysql` +- Check mount order in configuration + +--- + +#### Issue: Validation error "must have either mount_default_field or mnt_opts" + +**Cause:** Mount entry missing both profile reference and explicit mount options + +**Solution:** +- Add `mount_default_field: "profile_name"`, OR +- Add `mnt_opts: "mount_options"` + +--- + +## References + +### Related Files + +- **Configuration:** `input/storage_config.yml` +- **Schema:** `common/library/module_utils/input_validation/schema/storage_config.json` +- **Cloud-Init Template:** `discovery/roles/configure_ochami/templates/cloud_init/ci-group-*.yaml.j2` +- **PowerVault Setup Script:** Embedded in cloud-init template (`setup_iscsi_storage.sh`) + +### External Documentation + +- [VAST Data Documentation](https://support.vastdata.com/) +- [Dell PowerVault ME5 Series](https://www.dell.com/support/home/en-us/product-support/product/powervault-me5/docs) +- [Linux NFS Client Documentation](https://www.kernel.org/doc/Documentation/filesystems/nfs/nfs-client.txt) +- [Open-iSCSI Documentation](https://github.com/open-iscsi/open-iscsi) +- [XFS Filesystem Documentation](https://www.kernel.org/doc/html/latest/admin-guide/xfs.html) + +--- + +## Changelog + +### Version 1.0 (2026-03-20) + +- Initial design document +- Added VAST NFS profiles (`vast_nfs`, `vast_nfs_performance`) +- Added PowerVault iSCSI profile (`powervault_iscsi`) +- Removed `roles` field from profiles (roles only in mount entries) +- Changed `mount_default_fields` from array to mapping structure +- Added conditional validation (mount_default_field OR mnt_opts required) +- Updated examples to reflect VAST and PowerVault storage +- Aligned PowerVault examples with `setup_iscsi_storage.sh` implementation + +--- + +## Contact + +For questions or issues, please contact the Omnia development team. + +--- + +**End of Document** diff --git a/input/storage_config.yml b/input/storage_config.yml index 295fc6a0dc..d17cef561d 100644 --- a/input/storage_config.yml +++ b/input/storage_config.yml @@ -124,26 +124,39 @@ nfs_client_params: # Example configuration: # mounts: -# # Example 1: PowerVault iSCSI storage - Fully specified for Slurm persistent storage +# # Example 1: PowerVault iSCSI storage - Persistent storage (matches setup_iscsi_storage.sh) # - name: "powervault_slurm_persist" -# source: "/dev/mapper/mpatha1" # PowerVault multipath device partition +# source: "UUID=" # Or /dev/mapper/mpatha1 (partition created by setup script) # mount_point: "/mnt/slurm-persist" -# fs_type: "xfs" -# mnt_opts: "defaults,_netdev,noatime,x-systemd.requires=iscsi.service" -# dump_freq: "0" -# fsck_pass: "0" +# mount_default_field: "powervault_iscsi" +# # fs_type (xfs), mnt_opts, dump_freq, fsck_pass from powervault_iscsi profile # roles: ["slurm_control_node"] -# # All fields explicit - mount_default_field not needed # -# # Example 2: VAST NFS storage - Home directories using default profile +# # Example 2: PowerVault bind mount - MySQL data directory (from /mnt/slurm-persist/mysql) +# - name: "powervault_mysql_bind" +# source: "/mnt/slurm-persist/mysql" +# mount_point: "/var/lib/mysql" +# mount_default_field: "bind_mounts" +# # fs_type (none), mnt_opts (bind), dump_freq, fsck_pass from bind_mounts profile +# roles: ["slurm_control_node"] +# +# # Example 3: PowerVault bind mount - Slurm spool directory (from /mnt/slurm-persist/spool) +# - name: "powervault_spool_bind" +# source: "/mnt/slurm-persist/spool" +# mount_point: "/var/spool" +# mount_default_field: "bind_mounts" +# # fs_type (none), mnt_opts (bind), dump_freq, fsck_pass from bind_mounts profile +# roles: ["slurm_control_node"] +# +# # Example 4: VAST NFS storage - Home directories using default profile # - name: "vast_home" # source: "192.168.1.100:/home" # VAST NFS export # mount_point: "/home" -# mount_default_field: "default" -# # fs_type, mnt_opts, dump_freq, fsck_pass ALL from default profile +# mount_default_field: "vast_nfs" +# # fs_type (nfs4), mnt_opts, dump_freq, fsck_pass from vast_nfs profile # roles: ["slurm_control_node", "slurm_node", "login_node"] # -# # Example 3: VAST NFS storage - Shared applications with explicit fs_type +# # Example 5: VAST NFS storage - Shared applications with explicit fs_type # - name: "vast_apps" # source: "192.168.1.100:/apps" # VAST NFS export # mount_point: "/opt/apps" @@ -153,62 +166,42 @@ nfs_client_params: # # fs_type from network_storage is IGNORED (explicit wins) # roles: ["slurm_control_node", "slurm_node"] # -# # Example 4: VAST NFS storage - Read-only data with custom mount options -# - name: "vast_data_ro" -# source: "192.168.1.100:/datasets" # VAST NFS export -# mount_point: "/data" -# fs_type: "nfs4" # ← EXPLICIT -# mnt_opts: "defaults,nofail,_netdev,ro,noatime" # ← EXPLICIT (read-only) -# mount_default_field: "network_storage" -# # dump_freq, fsck_pass from network_storage profile -# # fs_type and mnt_opts from network_storage are IGNORED (explicit wins) -# roles: ["slurm_node"] -# -# # Example 5: PowerVault bind mount - MySQL data directory -# - name: "powervault_mysql_bind" -# source: "/mnt/slurm-persist/mysql" -# mount_point: "/var/lib/mysql" -# mount_default_field: "bind_mounts" -# # fs_type, mnt_opts, dump_freq, fsck_pass ALL from bind_mounts profile -# roles: ["slurm_control_node"] -# -# # Example 6: VAST NFS storage - Scratch space with high-performance options +# # Example 6: VAST NFS storage - High-performance scratch with performance profile # - name: "vast_scratch" # source: "192.168.1.100:/scratch" # VAST NFS export # mount_point: "/scratch" -# fs_type: "nfs4" -# mnt_opts: "defaults,nofail,_netdev,noatime,nodiratime,rsize=1048576,wsize=1048576" -# dump_freq: "0" -# fsck_pass: "0" +# mount_default_field: "vast_nfs_performance" +# # fs_type (nfs4), mnt_opts (with 1MB buffers), dump_freq, fsck_pass from vast_nfs_performance profile # roles: ["slurm_node"] mounts: [] - # Example 1: VAST NFS storage - Home directories using default NFS profile + # Example 1: PowerVault iSCSI storage - Persistent storage using profile + # - name: "powervault_slurm_persist" + # source: "UUID=" + # mount_point: "/mnt/slurm-persist" + # mount_default_field: "powervault_iscsi" + # # fs_type (xfs), mnt_opts, dump_freq, fsck_pass from powervault_iscsi profile + # roles: ["slurm_control_node"] + # + # Example 2: PowerVault bind mount - MySQL data directory + # - name: "powervault_mysql_bind" + # source: "/mnt/slurm-persist/mysql" + # mount_point: "/var/lib/mysql" + # mount_default_field: "bind_mounts" + # roles: ["slurm_control_node"] + # + # Example 3: VAST NFS storage - Home directories using VAST profile # - name: "vast_home" # source: "192.168.1.100:/home" # mount_point: "/home" - # mount_default_field: "default" - # # fs_type (nfs), mnt_opts, dump_freq, fsck_pass will be filled from default profile + # mount_default_field: "vast_nfs" # roles: ["slurm_control_node", "slurm_node", "login_node"] # - # Example 2: PowerVault iSCSI storage - Fully specified persistent storage - # - name: "powervault_slurm_persist" - # source: "/dev/mapper/mpatha1" #TODO: how to derive this? - # mount_point: "/mnt/slurm-persist" - # fs_type: "xfs" - # mnt_opts: "defaults,_netdev,noatime,x-systemd.requires=iscsi.service" - # dump_freq: "0" - # fsck_pass: "0" - # roles: ["slurm_control_node"] - # - # Example 3: VAST NFS storage - High-performance scratch with custom options + # Example 4: VAST NFS storage - High-performance scratch # - name: "vast_scratch" # source: "192.168.1.100:/scratch" # mount_point: "/scratch" - # fs_type: "nfs4" - # mnt_opts: "defaults,nofail,_netdev,noatime,nodiratime,rsize=1048576,wsize=1048576" - # dump_freq: "0" - # fsck_pass: "0" + # mount_default_field: "vast_nfs_performance" # roles: ["slurm_node"] # Named default profiles for mount configurations From d233ab55d5d92e0e4c7a3751717b4d4373572a45 Mon Sep 17 00:00:00 2001 From: Jagadeesh N V Date: Fri, 27 Mar 2026 17:40:55 +0530 Subject: [PATCH 08/10] STorage design almost finished --- STORAGE_DESIGN.md | 804 ++++++++++++------ .../schema/storage_config.json | 167 ++-- input/omnia_config.yml | 3 + input/powervault_reason_runcmd.md | 33 + input/storage_config.yml | 180 ++-- input/storage_profile.yml | 209 +++++ 6 files changed, 934 insertions(+), 462 deletions(-) create mode 100644 input/powervault_reason_runcmd.md create mode 100644 input/storage_profile.yml diff --git a/STORAGE_DESIGN.md b/STORAGE_DESIGN.md index 68f5d9436d..bad3a2e94f 100644 --- a/STORAGE_DESIGN.md +++ b/STORAGE_DESIGN.md @@ -1,8 +1,8 @@ # Storage Configuration Design -**Version:** 1.0 -**Date:** 2026-03-20 -**Status:** Design Complete +**Version:** 1.1 +**Date:** 2026-03-27 +**Status:** Design Active --- @@ -11,12 +11,13 @@ 1. [Overview](#overview) 2. [Architecture](#architecture) 3. [Configuration Schema](#configuration-schema) -4. [Mount Default Fields (Profiles)](#mount-default-fields-profiles) +4. [Mount Params (Profiles)](#mount-params-profiles) 5. [Storage Technologies](#storage-technologies) 6. [Usage Examples](#usage-examples) 7. [Priority Resolution](#priority-resolution) 8. [Best Practices](#best-practices) 9. [Validation Rules](#validation-rules) +10. [Design Decision: storage_config.yml vs storage_profile.yml](#design-decision-storage_configyml-vs-storage_profileyml) --- @@ -31,10 +32,12 @@ This design provides a flexible, profile-based mount configuration system for De ### Key Features -- ✅ **Profile-based configuration** - Reusable templates for common mount patterns +- ✅ **Profile-based configuration** - Reusable templates for common mount patterns (`mount_params`) - ✅ **Priority-based resolution** - Explicit values override profile defaults - ✅ **Vendor-specific optimizations** - VAST and PowerVault tuned profiles -- ✅ **Role-based targeting** - Mount configurations per node role +- ✅ **Functional-group targeting** - Mount configurations applied per node functional group prefix +- ✅ **Multi-volume PowerVault** - List-based `powervault_config` supports multiple iSCSI volumes +- ✅ **Dual PowerVault mount modes** - Reference an existing `mounts[]` entry or define inline - ✅ **Validation enforcement** - Schema validation ensures correct configuration --- @@ -49,26 +52,39 @@ This design provides a flexible, profile-based mount configuration system for De ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌────────────────────────────────────────────────────────┐ │ -│ │ mount_default_fields (Profiles) │ │ +│ │ mount_params (Profiles) │ │ │ │ ├─ vast_nfs │ │ │ │ ├─ vast_nfs_performance │ │ │ │ ├─ powervault_iscsi │ │ │ │ ├─ network_storage │ │ -│ │ └─ bind_mounts │ │ +│ │ ├─ bind_mounts │ │ +│ │ ├─ local_storage │ │ +│ │ ├─ scratch_storage │ │ +│ │ └─ global │ │ │ └────────────────────────────────────────────────────────┘ │ │ ▼ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ mounts (Mount Entries) │ │ │ │ ├─ vast_home (uses vast_nfs profile) │ │ -│ │ ├─ powervault_persist (uses powervault_iscsi) │ │ +│ │ ├─ powervault_slurm_persist (uses powervault_iscsi) │ │ │ │ └─ powervault_mysql_bind (uses bind_mounts) │ │ │ └────────────────────────────────────────────────────────┘ │ │ ▼ │ │ ┌────────────────────────────────────────────────────────┐ │ -│ │ powervault_config (Optional) │ │ -│ │ ├─ ip: [controller IPs] │ │ -│ │ ├─ iscsi_initiator: IQN │ │ -│ │ └─ volume_id: WWN │ │ +│ │ powervault_config (Optional, list) │ │ +│ │ ├─ name: powervault1 │ │ +│ │ │ ├─ ip / port / iscsi_initiator / volume_id │ │ +│ │ │ └─ mount: (Mode A) │ │ +│ │ └─ name: powervault2 │ │ +│ │ ├─ ip / port / iscsi_initiator / volume_id │ │ +│ │ └─ source / mount_point / mount_params / ... │ │ +│ │ (Mode B) │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ swap (Optional, list) │ │ +│ │ └─ name / filename / size / maxsize / │ │ +│ │ functional_group_prefix │ │ │ └────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ▼ @@ -84,7 +100,7 @@ This design provides a flexible, profile-based mount configuration system for De ▼ ┌──────────────────────────────────────┐ │ Node Provisioning │ - │ (Per Role) │ + │ (Per Functional Group) │ └──────────────────────────────────────┘ ``` @@ -97,61 +113,87 @@ This design provides a flexible, profile-based mount configuration system for De ```yaml # storage_config.yml -# PowerVault iSCSI configuration (optional) +# PowerVault iSCSI configuration (optional, list) powervault_config: - ip: [list of controller IPs] - port: 3260 - iscsi_initiator: "iqn.2025-01.com.dell:hostname" - volume_id: "00c0ff4343f1f1f1001c8c4e6901000000" + - name: "powervault1" + ip: + - 172.1.2.3 + port: 3260 + iscsi_initiator: "iqn.2025-01.com.dell:hostname" + volume_id: "00c0ff4343f1f1f1001c8c4e6901000000" + # Mode A: reference an existing mounts[] entry by name + mount: "powervault_slurm_persist" + + - name: "powervault2" + ip: + - 172.1.2.4 + port: 3260 + iscsi_initiator: "iqn.2025-01.com.dell:slurmd-node" + volume_id: "00c0ff4343f1f1f1001c8c4e6901000001" + # Mode B: define all mount parameters inline + source: "/dev/mapper/360002ac0000000000000000000000000" + mount_point: "/scratch" + mount_params: "powervault_iscsi" + fs_type: "xfs" + mnt_opts: "defaults,nofail" + functional_group_prefix: ["k8s_node"] -# Mount default profiles (templates) -mount_default_fields: +# Mount parameter profiles (templates) +mount_params: profile_name: fs_type: "filesystem_type" mnt_opts: "mount_options" dump_freq: "0" fsck_pass: "0" + # Optional custom fields (e.g., vast_nfs_ip, perf_ip) — used in mount templates # Mount entries mounts: - - name: "unique_mount_name" + - name: "unique_mount_name" # alphanumeric, underscore, hyphen only source: "device_or_network_path" mount_point: "/mount/path" - mount_default_field: "profile_name" # Optional - fs_type: "filesystem_type" # Optional (overrides profile) - mnt_opts: "mount_options" # Optional (overrides profile) - dump_freq: "0" # Optional (overrides profile) - fsck_pass: "0" # Optional (overrides profile) - roles: ["role1", "role2"] # Required + mount_params: "profile_name" # Optional — references mount_params profile + fs_type: "filesystem_type" # Optional — overrides profile + mnt_opts: "mount_options" # Optional — overrides profile + dump_freq: "0" # Optional — overrides profile + fsck_pass: "0" # Optional — overrides profile + functional_group_prefix: ["prefix1", "prefix2"] # Required + # All nodes whose functional group name starts with any listed prefix get this mount. + # e.g., ["slurm"] matches slurm_control_node, slurm_node, slurm_login, etc. + # Omit functional_group_prefix to apply the mount to ALL nodes (use with care). # Swap configuration (optional) swap: - name: "swap_name" filename: "/swapfile" size: "4G" - roles: ["role1"] + maxsize: "8G" # Optional — used when size is "auto" + functional_group_prefix: ["prefix1"] ``` --- -## Mount Default Fields (Profiles) +## Mount Params (Profiles) -Profiles are reusable templates that provide default values for mount configurations. They define the "HOW to mount" (technical settings), while mount entries define the "WHAT/WHERE/WHOM" (targeting and specifics). +`mount_params` are reusable templates that define the **how to mount** (technical settings). Mount entries define the **what, where, and who** (source, path, targeting). ### Profile Structure ```yaml -mount_default_fields: +mount_params: profile_name: - fs_type: "filesystem_type" # Required - mnt_opts: "mount_options" # Required - dump_freq: "0" # Required (usually "0") - fsck_pass: "0" # Required (usually "0" or "2") + fs_type: "filesystem_type" # Required + mnt_opts: "mount_options" # Required + dump_freq: "0" # Required + fsck_pass: "0" # Required + # Additional custom fields are allowed and are passed through to mount templates. + # Standard fstab fields (fs_type, mnt_opts, dump_freq, fsck_pass) are used directly + # by the cloud-init mounts module. Custom fields are available to Jinja2 templates. ``` ### Standard Profiles -#### 1. `default` - Standard NFS Defaults +#### 1. `default` — Standard NFS Defaults ```yaml default: @@ -161,11 +203,11 @@ default: fsck_pass: "0" ``` -**Use Case:** Generic NFS mounts with standard options +**Use Case:** Generic NFS mounts with standard options. --- -#### 2. `vast_nfs` - VAST NFS Standard Configuration +#### 2. `vast_nfs` — VAST NFS Standard Configuration ```yaml vast_nfs: @@ -173,17 +215,19 @@ vast_nfs: mnt_opts: "defaults,nofail,_netdev,noatime,x-systemd.after=cloud-init-network.service" dump_freq: "0" fsck_pass: "0" + vast_nfs_ip: "192.168.1.100" # Custom field — used in mount source templates ``` -**Use Case:** VAST Data NFS exports with standard performance +**Use Case:** VAST Data NFS exports with standard performance. **Features:** - NFSv4 protocol - `noatime` for improved performance - Network dependency handling +- `vast_nfs_ip` available as a template variable for resolving mount source --- -#### 3. `vast_nfs_performance` - VAST NFS High-Performance +#### 3. `vast_nfs_performance` — VAST NFS High-Performance ```yaml vast_nfs_performance: @@ -191,9 +235,10 @@ vast_nfs_performance: mnt_opts: "defaults,nofail,_netdev,noatime,nodiratime,rsize=1048576,wsize=1048576" dump_freq: "0" fsck_pass: "0" + perf_ip: "192.168.1.101" # Custom field — used in mount source templates ``` -**Use Case:** VAST Data NFS for high-throughput workloads (scratch, datasets) +**Use Case:** VAST Data NFS for high-throughput workloads (scratch, datasets). **Features:** - NFSv4 protocol - **1MB read/write buffers** (`rsize=1048576,wsize=1048576`) @@ -202,7 +247,7 @@ vast_nfs_performance: --- -#### 4. `powervault_iscsi` - PowerVault iSCSI Block Storage +#### 4. `powervault_iscsi` — PowerVault iSCSI Block Storage ```yaml powervault_iscsi: @@ -212,18 +257,18 @@ powervault_iscsi: fsck_pass: "0" ``` -**Use Case:** PowerVault iSCSI persistent storage +**Use Case:** PowerVault iSCSI persistent storage. **Features:** - XFS filesystem (high performance, scalability) - Requires iSCSI service to be running - `noatime` for improved performance - Network device handling -**Note:** Matches `setup_iscsi_storage.sh` default configuration +**Note:** Matches `setup_iscsi_storage.sh` default configuration. --- -#### 5. `network_storage` - Generic Network Storage +#### 5. `network_storage` — Generic Network Storage ```yaml network_storage: @@ -233,11 +278,11 @@ network_storage: fsck_pass: "0" ``` -**Use Case:** Generic network filesystems (NFS, CIFS, etc.) +**Use Case:** Generic network filesystems (NFS, CIFS, etc.). --- -#### 6. `local_storage` - Local Disk Storage +#### 6. `local_storage` — Local Disk Storage ```yaml local_storage: @@ -247,12 +292,12 @@ local_storage: fsck_pass: "2" ``` -**Use Case:** Local disks (ext4, xfs, etc.) -**Note:** `fsck_pass: "2"` enables filesystem check +**Use Case:** Local disks (ext4, xfs, etc.). +**Note:** `fsck_pass: "2"` enables filesystem check on boot. --- -#### 7. `bind_mounts` - Bind Mounts +#### 7. `bind_mounts` — Bind Mounts ```yaml bind_mounts: @@ -262,11 +307,11 @@ bind_mounts: fsck_pass: "0" ``` -**Use Case:** Bind mounts (e.g., `/mnt/slurm-persist/mysql` → `/var/lib/mysql`) +**Use Case:** Bind mounts (e.g., `/mnt/slurm-persist/mysql` → `/var/lib/mysql`). --- -#### 8. `scratch_storage` - High-Performance Scratch +#### 8. `scratch_storage` — High-Performance Scratch ```yaml scratch_storage: @@ -276,11 +321,11 @@ scratch_storage: fsck_pass: "2" ``` -**Use Case:** Local high-performance scratch storage +**Use Case:** Local high-performance scratch storage. --- -#### 9. `global` - Global Fallback +#### 9. `global` — Global Fallback ```yaml global: @@ -290,7 +335,7 @@ global: fsck_pass: "2" ``` -**Use Case:** Fallback when no specific profile matches +**Use Case:** Fallback when no specific profile matches and no explicit options are provided. --- @@ -307,14 +352,15 @@ mounts: - name: "vast_home" source: "192.168.1.100:/home" # VAST NFS export mount_point: "/home" - mount_default_field: "vast_nfs" - roles: ["slurm_control_node", "slurm_node", "login_node"] + mount_params: "vast_nfs" + functional_group_prefix: ["slurm"] ``` #### Source Format - **Pattern:** `:` - **Example:** `192.168.1.100:/home` +- **Template variable:** `{{ vast_nfs_ip }}:/home` (using `vast_nfs_ip` from `mount_params`) #### Recommended Profiles @@ -322,7 +368,7 @@ mounts: |----------|---------|--------| | Home directories | `vast_nfs` | Standard performance, shared access | | Shared applications | `vast_nfs` | Standard performance, read-heavy | -| Datasets (read-only) | `vast_nfs` | Standard performance, read-only | +| Datasets (read-only) | `vast_nfs` | Standard performance | | Scratch space | `vast_nfs_performance` | High throughput, large I/O | | Checkpoints | `vast_nfs_performance` | High throughput, write-heavy | @@ -345,46 +391,55 @@ mounts: **Overview:** Dell PowerVault provides block-level iSCSI storage for persistent data and databases. -#### Configuration Format +`powervault_config` is a **list**, allowing multiple volumes to be configured independently. Each volume entry supports two mount modes. + +#### Mode A: Reference an Existing `mounts[]` Entry + +Use `mount: ` to link the PowerVault volume to a fully-defined entry in the `mounts[]` list. The iSCSI setup provisions the device; the mount entry handles fstab/cloud-init configuration. ```yaml -# 1. Configure PowerVault connection powervault_config: - ip: - - 172.1.2.3 - - 172.1.2.4 - port: 3260 - iscsi_initiator: "iqn.2025-01.com.dell:scontrol-node" - volume_id: "00c0ff4343f1f1f1001c8c4e6901000000" - -# 2. Mount PowerVault persistent storage + - name: "powervault1" + ip: + - 172.1.2.3 + port: 3260 + iscsi_initiator: "iqn.2025-01.com.dell:scontrol-node" + volume_id: "00c0ff4343f1f1f1001c8c4e6901000000" + mount: "powervault_slurm_persist" # references mounts[] entry by name + mounts: - name: "powervault_slurm_persist" source: "UUID=" mount_point: "/mnt/slurm-persist" - mount_default_field: "powervault_iscsi" - roles: ["slurm_control_node"] + mount_params: "powervault_iscsi" + functional_group_prefix: ["slurm_control_node"] +``` - # 3. Bind mount for MySQL - - name: "powervault_mysql_bind" - source: "/mnt/slurm-persist/mysql" - mount_point: "/var/lib/mysql" - mount_default_field: "bind_mounts" - roles: ["slurm_control_node"] +#### Mode B: Inline Mount Definition + +Define all mount parameters directly on the `powervault_config` entry. Use this when the volume does not need a separate `mounts[]` entry. - # 4. Bind mount for Slurm spool - - name: "powervault_spool_bind" - source: "/mnt/slurm-persist/spool" - mount_point: "/var/spool" - mount_default_field: "bind_mounts" - roles: ["slurm_control_node"] +```yaml +powervault_config: + - name: "powervault2" + ip: + - 172.1.2.4 + port: 3260 + iscsi_initiator: "iqn.2025-01.com.dell:slurmd-node" + volume_id: "00c0ff4343f1f1f1001c8c4e6901000001" + source: "/dev/mapper/360002ac0000000000000000000000000" + mount_point: "/scratch" + mount_params: "powervault_iscsi" + fs_type: "xfs" + mnt_opts: "defaults,nofail" + functional_group_prefix: ["k8s_node"] ``` #### Source Format - **Pattern:** `UUID=` or `/dev/mapper/` - **Example:** `UUID=12345678-1234-1234-1234-123456789abc` -- **Note:** UUID is preferred for persistence +- **Note:** UUID is preferred over device paths for persistence across reboots. #### Setup Script Integration @@ -403,7 +458,7 @@ The `setup_iscsi_storage.sh` script automatically: #### Recommended Workflow ``` -PowerVault Setup +PowerVault Setup (setup_iscsi_storage.sh) ↓ /mnt/slurm-persist (XFS on /dev/mapper/mpatha1) ↓ @@ -411,149 +466,179 @@ PowerVault Setup └─ spool/ → bind mount to /var/spool ``` -#### Best Practices - -- ✅ Use UUID for source (more reliable than device paths) -- ✅ Use XFS filesystem (default in setup script) -- ✅ Create subdirectories for different services -- ✅ Use bind mounts to map subdirectories to system paths -- ✅ Ensure iSCSI service dependency in mount options - ---- - -## Usage Examples +#### Bind Mount Ordering -### Example 1: VAST Home Directories +Bind mounts that depend on a parent PowerVault mount **must appear after** the parent in the `mounts[]` list. The list is processed in order — if the parent is not yet mounted, the bind mount will fail. ```yaml mounts: - - name: "vast_home" - source: "192.168.1.100:/home" - mount_point: "/home" - mount_default_field: "vast_nfs" - roles: ["slurm_control_node", "slurm_node", "login_node"] -``` + # 1. Parent mount first + - name: "powervault_slurm_persist" + source: "UUID=" + mount_point: "/mnt/slurm-persist" + mount_params: "powervault_iscsi" + functional_group_prefix: ["slurm_control_node"] -**Result:** -- Filesystem: NFSv4 -- Mount options: `defaults,nofail,_netdev,noatime,x-systemd.after=cloud-init-network.service` -- Applied to: All Slurm nodes + # 2. Bind mounts after parent + - name: "powervault_mysql_bind" + source: "/mnt/slurm-persist/mysql" + mount_point: "/var/lib/mysql" + mount_params: "bind_mounts" + functional_group_prefix: ["slurm_control_node"] +``` --- -### Example 2: VAST High-Performance Scratch +## Usage Examples + +### Example 0: Atomic Mount (All Fields Explicit, No Profile) ```yaml mounts: - - name: "vast_scratch" - source: "192.168.1.100:/scratch" - mount_point: "/scratch" - mount_default_field: "vast_nfs_performance" - roles: ["slurm_node"] + - name: "atomic_nfs_data" + source: "UUID=" + mount_point: "/mnt/atomic" + fs_type: "nfs" + mnt_opts: "defaults,nofail,_netdev,x-systemd.after=cloud-init-network.service" + dump_freq: "0" + fsck_pass: "0" + functional_group_prefix: ["slurm_node"] ``` -**Result:** -- Filesystem: NFSv4 -- Mount options: `defaults,nofail,_netdev,noatime,nodiratime,rsize=1048576,wsize=1048576` -- Applied to: Compute nodes only -- Performance: 1MB read/write buffers +**Result:** All fields explicit; no profile lookup. Mount applies to all nodes whose functional group starts with `slurm_node`. --- -### Example 3: PowerVault Persistent Storage +### Example 1: PowerVault iSCSI — Mode A (Profile Reference via `mounts[]`) ```yaml powervault_config: - ip: - - 172.1.2.3 - - 172.1.2.4 - port: 3260 - iscsi_initiator: "iqn.2025-01.com.dell:scontrol-node" - volume_id: "00c0ff4343f1f1f1001c8c4e6901000000" + - name: "powervault1" + ip: + - 172.1.2.3 + port: 3260 + iscsi_initiator: "iqn.2025-01.com.dell:scontrol-node" + volume_id: "00c0ff4343f1f1f1001c8c4e6901000000" + mount: "powervault_slurm_persist" mounts: - name: "powervault_slurm_persist" source: "UUID=" mount_point: "/mnt/slurm-persist" - mount_default_field: "powervault_iscsi" - roles: ["slurm_control_node"] + mount_params: "powervault_iscsi" + functional_group_prefix: ["slurm_control_node_x86_64"] ``` **Result:** - Filesystem: XFS - Mount options: `defaults,_netdev,noatime,x-systemd.requires=iscsi.service` -- Applied to: Control node only -- Setup: Automated via `setup_iscsi_storage.sh` +- Applied to: Slurm control nodes on x86_64 architecture only --- -### Example 4: PowerVault Bind Mounts +### Example 2: PowerVault Bind Mount — MySQL Data Directory ```yaml mounts: - # Main persistent storage - - name: "powervault_slurm_persist" - source: "UUID=" - mount_point: "/mnt/slurm-persist" - mount_default_field: "powervault_iscsi" - roles: ["slurm_control_node"] - - # MySQL data directory - name: "powervault_mysql_bind" - source: "/mnt/slurm-persist/mysql" + source: "192.1.2.3:/mnt/mysql" mount_point: "/var/lib/mysql" - mount_default_field: "bind_mounts" - roles: ["slurm_control_node"] + mount_params: "bind_mounts" + functional_group_prefix: ["slurm_control_node"] +``` + +**Result:** +- Filesystem: none (bind) +- Mount options: `bind` +- Applied to: All Slurm control nodes (all architectures) + +--- - # Slurm spool directory - - name: "powervault_spool_bind" - source: "/mnt/slurm-persist/spool" - mount_point: "/var/spool" - mount_default_field: "bind_mounts" - roles: ["slurm_control_node"] +### Example 3: VAST NFS — Home Directories + +```yaml +mounts: + - name: "vast_home" + source: "{{ vast_nfs_ip }}:/home" # vast_nfs_ip resolved from vast_nfs profile + mount_point: "/home" + mount_params: "vast_nfs" + functional_group_prefix: ["slurm"] ``` **Result:** -- Main mount: `/mnt/slurm-persist` (XFS on iSCSI) -- Bind mount 1: `/var/lib/mysql` → `/mnt/slurm-persist/mysql` -- Bind mount 2: `/var/spool` → `/mnt/slurm-persist/spool` -- All on control node only +- Filesystem: NFSv4 +- Mount options: `defaults,nofail,_netdev,noatime,x-systemd.after=cloud-init-network.service` +- Applied to: All nodes whose functional group starts with `slurm` (control, compute, login, etc.) + +--- + +### Example 4: VAST NFS — High-Performance Scratch + +```yaml +mounts: + - name: "vast_scratch" + source: "192.168.1.100:/scratch" + mount_point: "/scratch" + mount_params: "vast_nfs_performance" + functional_group_prefix: ["k8s_node"] +``` + +**Result:** +- Filesystem: NFSv4 +- Mount options: `defaults,nofail,_netdev,noatime,nodiratime,rsize=1048576,wsize=1048576` +- Applied to: All nodes whose functional group starts with `k8s_node` +- Performance: 1MB read/write buffers --- -### Example 5: Explicit Override +### Example 5: Explicit Override of Profile Fields ```yaml mounts: - - name: "vast_apps_custom" + - name: "vast_apps_readonly" source: "192.168.1.100:/apps" mount_point: "/opt/apps" - fs_type: "nfs4" # ← EXPLICIT - mnt_opts: "defaults,nofail,_netdev,ro" # ← EXPLICIT (read-only) - mount_default_field: "network_storage" - # Only dump_freq and fsck_pass from profile - roles: ["slurm_control_node", "slurm_node"] + fs_type: "nfs4" # EXPLICIT — overrides profile + mnt_opts: "defaults,nofail,_netdev,ro" # EXPLICIT — overrides profile + mount_params: "network_storage" + # dump_freq and fsck_pass still come from network_storage profile + functional_group_prefix: ["slurm"] ``` **Result:** -- Filesystem: NFSv4 (explicit, not from profile) +- Filesystem: NFSv4 (explicit, profile value ignored) - Mount options: `defaults,nofail,_netdev,ro` (explicit, read-only) -- Dump frequency: `0` (from profile) -- Fsck pass: `0` (from profile) +- Dump frequency: `0` (from `network_storage` profile) +- Fsck pass: `0` (from `network_storage` profile) + +--- + +### Example 6: Swap Configuration + +```yaml +swap: + - name: "compute_swap" + filename: "/swapfile" + size: "2G" + maxsize: "4G" + functional_group_prefix: ["slurm_node"] +``` + +**Result:** A 2G swap file (up to 4G) created at `/swapfile` on all nodes whose functional group starts with `slurm_node`. --- ## Priority Resolution -When a mount entry references a profile, field values are resolved using this priority order: +When a mount entry references a profile via `mount_params`, field values are resolved using this priority order: ### Priority Order (Highest to Lowest) ``` 1. Explicit value in mount entry ← HIGHEST PRIORITY -2. Value from mount_default_field profile +2. Value from mount_params profile (if specified) 3. Auto-selected profile based on fs_type -4. Global fallback profile +4. Global fallback profile (global) 5. Hardcoded system defaults ← LOWEST PRIORITY ``` @@ -562,7 +647,7 @@ When a mount entry references a profile, field values are resolved using this pr #### Example 1: Full Profile Usage ```yaml -mount_default_fields: +mount_params: vast_nfs: fs_type: "nfs4" mnt_opts: "defaults,nofail,_netdev,noatime" @@ -573,8 +658,8 @@ mounts: - name: "vast_home" source: "192.168.1.100:/home" mount_point: "/home" - mount_default_field: "vast_nfs" - roles: ["slurm_node"] + mount_params: "vast_nfs" + functional_group_prefix: ["slurm_node"] ``` **Resolution:** @@ -588,7 +673,7 @@ mounts: #### Example 2: Partial Override ```yaml -mount_default_fields: +mount_params: vast_nfs: fs_type: "nfs4" mnt_opts: "defaults,nofail,_netdev,noatime" @@ -601,38 +686,33 @@ mounts: mount_point: "/opt/apps" fs_type: "nfs4" # ← EXPLICIT mnt_opts: "defaults,nofail,_netdev,ro" # ← EXPLICIT - mount_default_field: "vast_nfs" - roles: ["slurm_node"] + mount_params: "vast_nfs" + functional_group_prefix: ["slurm_node"] ``` **Resolution:** -- `fs_type`: `"nfs4"` ← **EXPLICIT (priority 1)** - profile value ignored -- `mnt_opts`: `"defaults,nofail,_netdev,ro"` ← **EXPLICIT (priority 1)** - profile value ignored +- `fs_type`: `"nfs4"` ← **EXPLICIT (priority 1)** — profile value ignored +- `mnt_opts`: `"defaults,nofail,_netdev,ro"` ← **EXPLICIT (priority 1)** — profile value ignored - `dump_freq`: `"0"` ← from `vast_nfs` profile (priority 2) - `fsck_pass`: `"0"` ← from `vast_nfs` profile (priority 2) --- -#### Example 3: No Profile Specified +#### Example 3: No Profile — All Fields Explicit ```yaml mounts: - - name: "vast_data" + - name: "atomic_data" source: "192.168.1.100:/data" mount_point: "/data" fs_type: "nfs4" mnt_opts: "defaults,nofail,_netdev" dump_freq: "0" fsck_pass: "0" - roles: ["slurm_node"] + functional_group_prefix: ["slurm_node"] ``` -**Resolution:** -- `fs_type`: `"nfs4"` ← EXPLICIT (priority 1) -- `mnt_opts`: `"defaults,nofail,_netdev"` ← EXPLICIT (priority 1) -- `dump_freq`: `"0"` ← EXPLICIT (priority 1) -- `fsck_pass`: `"0"` ← EXPLICIT (priority 1) -- No profile needed - all fields explicit +**Resolution:** All four fstab fields are explicit (priority 1). No profile lookup is performed. --- @@ -643,12 +723,13 @@ mounts: ✅ **DO:** - Create profiles for common storage patterns - Use descriptive profile names (`vast_nfs`, `powervault_iscsi`) +- Add custom fields (e.g., `vast_nfs_ip`) for template variables - Document profile purpose and use cases -- Keep profiles simple and focused +- Keep profiles simple and focused on a single storage technology ❌ **DON'T:** -- Include roles in profiles (roles belong in mount entries) -- Create too many similar profiles +- Include `functional_group_prefix` in profiles — it belongs in mount entries +- Create too many near-identical profiles - Use generic names like `profile1`, `profile2` --- @@ -656,17 +737,34 @@ mounts: ### Mount Configuration ✅ **DO:** -- Use profiles for standard configurations -- Override specific fields when needed -- Use UUID for PowerVault sources -- Specify roles for each mount -- Use descriptive mount names +- Use profiles (`mount_params`) for standard configurations +- Override specific fields when needed (e.g., force `ro`) +- Use UUID for PowerVault sources — more reliable than device paths +- Specify `functional_group_prefix` on every mount entry +- Use descriptive, alphanumeric mount names (no spaces) +- Order bind mounts **after** their parent mount in the list ❌ **DON'T:** -- Duplicate mount options across entries (use profiles) -- Mix explicit and profile values unnecessarily -- Use device paths for PowerVault (use UUID) -- Forget to specify roles +- Duplicate mount options across many entries — use profiles +- Use device paths (`/dev/sda1`) for PowerVault — use UUID +- Omit `functional_group_prefix` unless you intentionally want all-nodes application +- Place bind mounts before their parent in the `mounts[]` list + +--- + +### functional_group_prefix Targeting + +The `functional_group_prefix` field uses prefix matching against node functional group names: + +| Prefix | Matches | +|--------|---------| +| `["slurm"]` | `slurm_control_node`, `slurm_node`, `slurm_login`, `slurm_control_node_x86_64`, ... | +| `["slurm_control_node"]` | `slurm_control_node`, `slurm_control_node_x86_64`, `slurm_control_node_aarch64` | +| `["slurm_control_node_x86_64"]` | `slurm_control_node_x86_64` only | +| `["k8s"]` | `k8s_node`, `k8s_control_node`, `k8s_worker`, ... | +| `["slurm", "k8s"]` | All Slurm nodes AND all K8s nodes | + +Use the most specific prefix needed. Broader prefixes like `["slurm"]` apply to all Slurm node types across all architectures. --- @@ -680,7 +778,9 @@ mounts: | Large datasets | VAST NFS | `vast_nfs_performance` | | Persistent databases | PowerVault iSCSI | `powervault_iscsi` | | Slurm state files | PowerVault iSCSI | `powervault_iscsi` | +| Service bind mounts | PowerVault subdirectory | `bind_mounts` | | Local scratch | Local disk | `scratch_storage` | +| Generic network FS | NFS/CIFS | `network_storage` | --- @@ -702,7 +802,7 @@ mounts: **Buffer Size Tuning:** ```yaml -# Standard: Default buffers (typically 32KB-128KB) +# Standard: default buffers (typically 32KB–128KB) mnt_opts: "defaults,nofail,_netdev,noatime" # High-performance: 1MB buffers @@ -712,8 +812,8 @@ mnt_opts: "defaults,nofail,_netdev,noatime,nodiratime,rsize=1048576,wsize=104857 #### PowerVault iSCSI **Filesystem Choice:** -- ✅ **XFS** (recommended) - High performance, scalability, large files -- ⚠️ **ext4** - Good compatibility, lower performance at scale +- ✅ **XFS** (recommended) — High performance, scalability, large files +- ⚠️ **ext4** — Good compatibility, lower performance at scale **Mount Options:** ```yaml @@ -730,31 +830,51 @@ mnt_opts: "defaults,_netdev,noatime,nobarrier,x-systemd.requires=iscsi.service" ### Required Fields -#### Mount Entry +#### Mount Entry (`mounts[]`) -- ✅ `name` - Unique identifier -- ✅ `source` - Device or network path -- ✅ `mount_point` - Mount point path -- ✅ `roles` - List of target roles +- ✅ `name` — Unique identifier (alphanumeric, `_`, `-`; no spaces) +- ✅ `source` — Device or network path +- ✅ `mount_point` — Absolute path starting with `/` +- ✅ `functional_group_prefix` — List of functional group prefixes - ✅ **At least one of:** - - `mount_default_field` (references a profile), OR + - `mount_params` (references a `mount_params` profile), **OR** - `mnt_opts` (explicit mount options) -#### Profile +#### PowerVault Entry (`powervault_config[]`) + +- ✅ `name` — Unique identifier for this volume +- ✅ `ip` — List of controller IPv4 addresses (min 1) +- ✅ `iscsi_initiator` — IQN string +- ✅ `volume_id` — Hex WWN string +- ⬜ `port` — Optional; defaults to `3260` +- ✅ **Exactly one of:** + - `mount: ` (Mode A — references a `mounts[]` entry), **OR** + - Inline fields: `source`, `mount_point`, `mount_params` / `mnt_opts`, `functional_group_prefix` (Mode B) + +#### Swap Entry (`swap[]`) + +- ✅ `name` — Unique identifier +- ✅ `filename` — Absolute path for the swap file +- ✅ `size` — Human-readable size (`2G`, `512M`, `auto`) +- ⬜ `maxsize` — Optional; used only when `size: auto` +- ✅ `functional_group_prefix` — List of functional group prefixes -- ✅ `fs_type` - Filesystem type -- ✅ `mnt_opts` - Mount options -- ✅ `dump_freq` - Dump frequency -- ✅ `fsck_pass` - Fsck pass number +#### Profile (`mount_params`) + +- ✅ `fs_type` — Filesystem type +- ✅ `mnt_opts` — Mount options +- ✅ `dump_freq` — Dump frequency +- ✅ `fsck_pass` — Fsck pass number +- ⬜ Custom fields (e.g., `vast_nfs_ip`, `perf_ip`) — Optional; passed to Jinja2 templates --- ### Field Validation -#### `name` +#### `name` (mount or swap) - Pattern: `^[a-zA-Z0-9_-]+$` -- Length: 1-64 characters -- Must be unique across all mounts +- Length: 1–64 characters +- Must be unique across all entries in the same list #### `source` - Minimum length: 1 character @@ -762,6 +882,7 @@ mnt_opts: "defaults,_netdev,noatime,nobarrier,x-systemd.requires=iscsi.service" - `/dev/sda1` - `UUID=12345678-1234-1234-1234-123456789abc` - `192.168.1.100:/export/share` + - `{{ vast_nfs_ip }}:/home` (Jinja2 template resolved at generation time) #### `mount_point` - Pattern: `^/[a-zA-Z0-9/_.-]*$` @@ -772,7 +893,7 @@ mnt_opts: "defaults,_netdev,noatime,nobarrier,x-systemd.requires=iscsi.service" #### `mnt_opts` - Pattern: `^[a-zA-Z0-9,=._-]+$` -- Examples: `defaults,nofail,_netdev` +- Examples: `defaults,nofail,_netdev`, `bind`, `defaults,nofail,noatime` #### `dump_freq` - Pattern: `^[0-2]$` @@ -781,39 +902,25 @@ mnt_opts: "defaults,_netdev,noatime,nobarrier,x-systemd.requires=iscsi.service" #### `fsck_pass` - Pattern: `^[0-9]$` - Common values: - - `0` - No fsck (network filesystems, bind mounts) - - `1` - Root filesystem - - `2` - Other local filesystems + - `0` — No fsck (network filesystems, bind mounts) + - `1` — Root filesystem + - `2` — Other local filesystems -#### `roles` +#### `functional_group_prefix` - Array of strings -- Pattern per role: `^[a-zA-Z0-9_-]+$` +- Pattern per element: `^[a-zA-Z0-9_-]+$` - Must be unique within array -- Examples: `slurm_control_node`, `slurm_node`, `login_node` +- Prefix-matched against node functional group names at provisioning time ---- +#### `volume_id` (PowerVault) +- Pattern: `^[a-fA-F0-9]+$` -### Profile Validation +#### `iscsi_initiator` (PowerVault) +- Pattern: `^iqn\.[a-zA-Z0-9.-]+(?::[a-zA-Z0-9._:-]+)?$` -#### Profile Name (Key) -- Pattern: `^[a-zA-Z0-9_-]+$` -- Must be unique across all profiles -- Examples: `vast_nfs`, `powervault_iscsi`, `network_storage` - -#### Profile Structure -```json -{ - "type": "object", - "properties": { - "fs_type": { "type": "string", "enum": [...] }, - "mnt_opts": { "type": "string", "pattern": "^[a-zA-Z0-9,=._-]+$" }, - "dump_freq": { "type": "string", "pattern": "^[0-2]$" }, - "fsck_pass": { "type": "string", "pattern": "^[0-9]$" } - }, - "required": ["fs_type", "mnt_opts", "dump_freq", "fsck_pass"], - "additionalProperties": false -} -``` +#### `size` (swap) +- Pattern: `^(auto|[0-9]+[BKMGT]?)$` +- Examples: `2G`, `512M`, `auto` --- @@ -824,7 +931,7 @@ mnt_opts: "defaults,_netdev,noatime,nobarrier,x-systemd.requires=iscsi.service" ```json { "anyOf": [ - { "required": ["mount_default_field"] }, + { "required": ["mount_params"] }, { "required": ["mnt_opts"] } ] } @@ -832,36 +939,36 @@ mnt_opts: "defaults,_netdev,noatime,nobarrier,x-systemd.requires=iscsi.service" **Valid:** ```yaml -# Has mount_default_field +# Has mount_params profile - name: "mount1" source: "..." mount_point: "..." - mount_default_field: "vast_nfs" - roles: [...] + mount_params: "vast_nfs" + functional_group_prefix: ["slurm"] -# Has mnt_opts +# Has mnt_opts explicit - name: "mount2" source: "..." mount_point: "..." mnt_opts: "defaults,nofail" - roles: [...] + functional_group_prefix: ["slurm"] -# Has both (explicit wins) +# Has both — explicit wins for mnt_opts, profile fills the rest - name: "mount3" source: "..." mount_point: "..." - mount_default_field: "vast_nfs" - mnt_opts: "defaults,nofail,ro" # Overrides profile - roles: [...] + mount_params: "vast_nfs" + mnt_opts: "defaults,nofail,ro" # overrides profile's mnt_opts + functional_group_prefix: ["slurm"] ``` **Invalid:** ```yaml -# Missing both mount_default_field and mnt_opts +# Missing both mount_params and mnt_opts - name: "mount_invalid" source: "..." mount_point: "..." - roles: [...] + functional_group_prefix: ["slurm"] ``` --- @@ -876,8 +983,8 @@ mnt_opts: "defaults,_netdev,noatime,nobarrier,x-systemd.requires=iscsi.service" | **VAST** | Shared apps | `vast_nfs` | NFSv4, standard perf | | **VAST** | Scratch space | `vast_nfs_performance` | NFSv4, 1MB buffers | | **VAST** | Large datasets | `vast_nfs_performance` | NFSv4, 1MB buffers | -| **PowerVault** | Persistent storage | `powervault_iscsi` | XFS, iSCSI service | -| **PowerVault** | Database bind | `bind_mounts` | Bind from persistent | +| **PowerVault** | Persistent storage | `powervault_iscsi` | XFS, iSCSI service dep | +| **PowerVault** | Database bind | `bind_mounts` | Bind from persistent mount | | **Generic** | Network FS | `network_storage` | Auto-detect FS | | **Local** | Local disk | `local_storage` | Auto-detect FS | | **Local** | Scratch | `scratch_storage` | XFS, optimized | @@ -923,7 +1030,7 @@ mnt_opts: "defaults,_netdev,noatime,nobarrier,x-systemd.requires=iscsi.service" #### Issue: Mount fails with "mount.nfs: Connection timed out" -**Cause:** Network not ready or VAST server unreachable +**Cause:** Network not ready or VAST server unreachable. **Solution:** - Ensure `_netdev` and `x-systemd.after=cloud-init-network.service` in mount options @@ -934,7 +1041,7 @@ mnt_opts: "defaults,_netdev,noatime,nobarrier,x-systemd.requires=iscsi.service" #### Issue: PowerVault mount fails with "No such device" -**Cause:** iSCSI service not running or multipath device not ready +**Cause:** iSCSI service not running or multipath device not ready. **Solution:** - Ensure `x-systemd.requires=iscsi.service` in mount options @@ -946,25 +1053,34 @@ mnt_opts: "defaults,_netdev,noatime,nobarrier,x-systemd.requires=iscsi.service" #### Issue: Bind mount fails with "mount point does not exist" -**Cause:** Source directory doesn't exist +**Cause:** Source directory does not exist (parent mount not yet mounted, or list ordering issue). **Solution:** -- Ensure parent mount is mounted first -- Create source directory: `mkdir -p /mnt/slurm-persist/mysql` -- Check mount order in configuration +- Ensure the parent mount entry appears **before** the bind mount in `mounts[]` +- Create the source directory: `mkdir -p /mnt/slurm-persist/mysql` +- Check that the parent PowerVault mount is healthy --- -#### Issue: Validation error "must have either mount_default_field or mnt_opts" +#### Issue: Validation error "must have either mount_params or mnt_opts" -**Cause:** Mount entry missing both profile reference and explicit mount options +**Cause:** Mount entry is missing both a profile reference and explicit mount options. **Solution:** -- Add `mount_default_field: "profile_name"`, OR +- Add `mount_params: "profile_name"`, **OR** - Add `mnt_opts: "mount_options"` --- +#### Issue: Mount name fails validation + +**Cause:** Mount `name` contains spaces or special characters (e.g., `"Atomic mount"`). + +**Solution:** +- Use only alphanumeric characters, underscores, and hyphens: `"atomic_mount"` + +--- + ## References ### Related Files @@ -973,19 +1089,169 @@ mnt_opts: "defaults,_netdev,noatime,nobarrier,x-systemd.requires=iscsi.service" - **Schema:** `common/library/module_utils/input_validation/schema/storage_config.json` - **Cloud-Init Template:** `discovery/roles/configure_ochami/templates/cloud_init/ci-group-*.yaml.j2` - **PowerVault Setup Script:** Embedded in cloud-init template (`setup_iscsi_storage.sh`) +- **Cluster Configuration:** `input/omnia_config.yml` (references mount names via `mounts:` list) + +--- + +## Design Decision: storage_config.yml vs storage_profile.yml + +This section documents the evaluation of two proposed input designs for filling the cloud-init `mounts:` module in oChaMI per-group cloud-init data, and explains the rationale for choosing `storage_config.yml`. + +--- -### External Documentation +### Goal + +oChaMI provisions nodes by pushing per-group cloud-init payloads. Each payload's `mounts:` module requires a flat list of fstab tuples: + +```yaml +mounts: + - [source, mount_point, fs_type, mnt_opts, dump_freq, fsck_pass] + - [source, mount_point, fs_type, mnt_opts, dump_freq, fsck_pass] +``` -- [VAST Data Documentation](https://support.vastdata.com/) -- [Dell PowerVault ME5 Series](https://www.dell.com/support/home/en-us/product-support/product/powervault-me5/docs) -- [Linux NFS Client Documentation](https://www.kernel.org/doc/Documentation/filesystems/nfs/nfs-client.txt) -- [Open-iSCSI Documentation](https://github.com/open-iscsi/open-iscsi) -- [XFS Filesystem Documentation](https://www.kernel.org/doc/html/latest/admin-guide/xfs.html) +The design must be **simple and minimal** — the template engine should be able to produce this list for any given functional group without complex logic, missing fields, or ambiguous conventions. + +--- + +### Resolution Path Comparison + +#### `storage_config.yml` — 1-hop resolution + +``` +functional_group name (e.g. slurm_node_x86_64) + │ + ▼ + scan mounts[] where functional_group_prefix prefix-matches the group name + │ + ▼ (one direct pass) + for each matched mount entry: + resolve [source, mount_point, fs_type, mnt_opts, dump_freq, fsck_pass] + via: explicit field → mount_params profile → global default + │ + ▼ + flat list of tuples ──► cloud-init mounts: +``` + +One loop. One dict merge per entry. No construction, no inference. + +#### `storage_profile.yml` — 4-hop resolution + +``` +functional_group name (e.g. slurm_compute_x86_64) + │ + ▼ + mounts[cluster][functional_group] → profile name(s) + │ + ▼ + storage_profiles[profile][backend_ref] → path map + │ + ▼ + storage_config[section][backend_ref] → ip(s), options, protocol + │ + ▼ + construct source = ip + server_path + mount_point = client_path ← direction undocumented + fs_type = inferred from protocol ← not explicit + mnt_opts = options from backend ← no per-path override possible + dump_freq = ??? ← missing entirely + fsck_pass = ??? ← missing entirely + │ + ▼ + flat list of tuples ──► cloud-init mounts: +``` + +Four hops, path direction ambiguity, two required fstab fields absent. + +--- + +### Field-by-Field Comparison + +| cloud-init field | `storage_config.yml` | `storage_profile.yml` | +|---|---|---| +| `source` | Explicit on mount entry | Constructed: `ip + server_path` from `storage_config` | +| `mount_point` | Explicit on mount entry | Ambiguous: `"/home": "/home"` — which is server, which is client? | +| `fs_type` | Explicit or from `mount_params` profile | Must be inferred from `protocol` field | +| `mnt_opts` | Explicit or from `mount_params` profile | `options` from backend config; no per-path overrides | +| `dump_freq` | Explicit or from profile | **Not present anywhere in the file** | +| `fsck_pass` | Explicit or from profile | **Not present anywhere in the file** | +| Node targeting | `functional_group_prefix` per mount | `mounts[cluster][fg_name]` → profile → backend (3 levels) | +| Resolution hops | **1 (direct match + profile merge)** | **4 (cluster → fg → profile → backend)** | +| Template complexity | Low — filter loop + dict merge | High — nested loops, type dispatch, fallback inference | + +--- + +### Issues Found in Each Approach + +#### `storage_config.yml` — Fixable Issues + +| Issue | Severity | Fix | +|---|---|---| +| `mount_params` key diverges from JSON schema (`mount_default_fields`) | High | Update schema to match file | +| `omnia_config.yml` references `nfs_slurm`, `nfs_home` — neither exists as a mount `name` | High | Add matching named entries or align names | +| `"Atomic mount"` name contains a space — fails schema pattern | Medium | Rename to `atomic_mount` | +| `functional_group_prefix` omitted on one entry — undefined all-nodes behavior | Medium | Document or enforce requirement | +| Bind mount ordering not enforced — depends on list position | Low | Document ordering requirement | +| `vast_nfs_ip`, `perf_ip` custom fields in profiles — schema `additionalProperties: false` rejects them | Medium | Relax schema for custom fields | + +All issues are mechanical — wrong key names, missing entries, schema not updated after design evolved. + +#### `storage_profile.yml` — Structural Issues + +| Issue | Severity | Fix | +|---|---|---| +| Path map direction `"/home": "/home"` undocumented — server:client or client:server? | **Critical** | No fix without redesign | +| PowerVault entries are scalars (`pv1_volume1: "/var/lib/mysql"`) vs NFS entries are maps — structurally inconsistent | **Critical** | No fix without redesign | +| `dump_freq` and `fsck_pass` absent — cannot generate valid fstab tuple | **Critical** | Requires new fields added throughout | +| `slurm_common_profile` is null — silently applies nothing | High | Validate against null profiles | +| `slurm_login_x86_64` is null — login nodes silently get no mounts | High | Validate against null node assignments | +| `nfs1` and `nfs2` are identical copies — `slurm_compute_profile_aarch64` resolves to same paths as base | Medium | Copy-paste error | +| `ps1` uses `ips:` (list), `ps2` uses `ip:` (scalar) — inconsistent field names | Medium | Standardize field name | +| Multi-profile list (`- slurm_compute_profile\n- slurm_compute_profile_aarch64`) has no merge strategy — `/home` collision | High | Define merge semantics | +| `/tmp` mounted from VAST NFS in compute and compiler profiles — network `/tmp` is a reliability risk in HPC | Medium | Design smell | + +The structural issues (path direction, missing fstab fields, inconsistent value types) cannot be fixed by adding fields — they require reconceiving the profile format. + +--- + +### Why `storage_config.yml` Wins for This Use Case + +**1. Shape matches the output.** Each `mounts[]` entry is already one fstab tuple. The Jinja2 template is a filter — no construction or inference. `storage_profile.yml` requires building the tuple from scattered pieces across three separate data structures. + +**2. All six fstab fields are present.** `source`, `mount_point`, `fs_type`, `mnt_opts`, `dump_freq`, `fsck_pass` are either explicit on the entry or resolved from `mount_params`. `storage_profile.yml` is missing `dump_freq` and `fsck_pass` entirely. + +**3. `functional_group_prefix` is the right targeting primitive.** oChaMI groups nodes by functional group name. Prefix matching (`["slurm"]` matches `slurm_node_x86_64`, `slurm_control_node_aarch64`, etc.) handles architecture variants without enumerating every combination. `storage_profile.yml`'s cluster → role mapping adds indirection that only matters if clusters are topologically distinct — which is already handled by `omnia_config.yml`. + +**4. `mount_params` profiles provide reuse without indirection.** The VAST RDMA flags (`rsize`, `wsize`, `noatime`) live in one profile and are inherited by all referencing mounts. This is the same reuse benefit `storage_profile.yml` claims for its role profiles, but without the extra lookup hop. + +**5. `storage_profile.yml`'s multi-cluster advantage is already covered.** `omnia_config.yml` manages cluster topology. Each cluster entry references mount names from `storage_config.yml`. Cluster-level scoping belongs in the cluster config, not the storage config. + +--- + +### Conclusion + +> **`storage_config.yml` is the chosen design.** It directly satisfies the requirement of filling the cloud-init `mounts:` module per functional group with minimal template complexity and zero ambiguity. The issues found are all fixable with targeted corrections to key names and schema alignment. `storage_profile.yml`'s issues are structural and cannot produce valid fstab tuples without a fundamental redesign. --- ## Changelog +### Version 1.1 (2026-03-27) + +- Renamed `mount_default_fields` → `mount_params` throughout (matches `storage_config.yml`) +- Renamed `mount_default_field` → `mount_params` on mount entries +- Renamed `roles` → `functional_group_prefix` on mount and swap entries +- Updated `powervault_config` from single object to **list** (supports multiple volumes) +- Documented dual PowerVault mount modes: Mode A (`mount:` reference) and Mode B (inline fields) +- Added custom fields (`vast_nfs_ip`, `perf_ip`) to `vast_nfs` and `vast_nfs_performance` profiles +- Added `nfs_bind_mounts` profile reference in examples +- Added Example 0 (atomic mount with all explicit fields) +- Added Example 6 (swap configuration) +- Added `functional_group_prefix` targeting reference table +- Added bind mount ordering requirement and warning +- Added troubleshooting entry for invalid mount names (spaces) +- Updated Architecture diagram to reflect list-based `powervault_config` and `swap` +- Updated all validation rules to match current field names and structures + ### Version 1.0 (2026-03-20) - Initial design document @@ -993,7 +1259,7 @@ mnt_opts: "defaults,_netdev,noatime,nobarrier,x-systemd.requires=iscsi.service" - Added PowerVault iSCSI profile (`powervault_iscsi`) - Removed `roles` field from profiles (roles only in mount entries) - Changed `mount_default_fields` from array to mapping structure -- Added conditional validation (mount_default_field OR mnt_opts required) +- Added conditional validation (`mount_default_field` OR `mnt_opts` required) - Updated examples to reflect VAST and PowerVault storage - Aligned PowerVault examples with `setup_iscsi_storage.sh` implementation @@ -1004,5 +1270,3 @@ mnt_opts: "defaults,_netdev,noatime,nobarrier,x-systemd.requires=iscsi.service" For questions or issues, please contact the Omnia development team. --- - -**End of Document** diff --git a/common/library/module_utils/input_validation/schema/storage_config.json b/common/library/module_utils/input_validation/schema/storage_config.json index bed03ea250..a0dd9a37c0 100644 --- a/common/library/module_utils/input_validation/schema/storage_config.json +++ b/common/library/module_utils/input_validation/schema/storage_config.json @@ -3,84 +3,53 @@ "title": "Configuration Schema", "type": "object", "properties": { - "nfs_client_params": { + "powervault_config": { "type": "array", + "description": "List of PowerVault iSCSI volume connection definitions", "items": { "type": "object", "properties": { - "nfs_name": { + "name": { "type": "string", - "description": "The unique NFS server name" + "description": "Unique identifier for this PowerVault volume", + "pattern": "^[a-zA-Z0-9_-]+$", + "minLength": 1, + "maxLength": 64 }, - "server_ip": { - "type": "string", - "anyOf": [ - { - "allOf": [ - { "pattern": ".*[A-Za-z].*" }, - { "format": "idn-hostname" } - ] - }, - { - "allOf": [ - { "pattern": "^[0-9.]+$" }, - { "format": "ipv4" } - ] - } - ] + "ip": { + "description": "List of target controller IP addresses", + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "format": "ipv4" + }, + "uniqueItems": true }, - "server_share_path": { - "type": "string", - "pattern": "^/(?:[^/]+(?:/[^/]+)*)?/?$" + "port": { + "description": "TCP port for iSCSI target (default 3260)", + "type": "integer", + "minimum": 1, + "maximum": 65535 }, - "client_share_path": { + "iscsi_initiator": { + "description": "iSCSI initiator IQN", "type": "string", - "pattern": "^/(?:[^/]+(?:/[^/]+)*)?/?$" + "pattern": "^iqn\\.[a-zA-Z0-9.-]+(?::[a-zA-Z0-9._:-]+)?$" }, - "client_mount_options": { - "type": "string" - } - }, - "required": [ - "server_ip", - "server_share_path", - "client_share_path", - "client_mount_options" - ] - }, - "minItems": 1 - }, - "powervault_config": { - "type": "object", - "required": ["ip", "iscsi_initiator", "volume_id"], - "properties": { - "ip": { - "description": "List of target controller IP addresses", - "type": "array", - "minItems": 1, - "items": { + "volume_id": { + "description": "Volume identifier (hex string / WWN)", "type": "string", - "format": "ipv4" + "pattern": "^[a-fA-F0-9]+$" }, - "uniqueItems": true - }, - - "port": { - "description": "TCP port for iSCSI (default 3260)", - "type": "integer" - }, - - "iscsi_initiator": { - "description": "iSCSI initiator IQN", - "type": "string", - "pattern": "^iqn\\.[a-zA-Z0-9.-]+(?::[a-zA-Z0-9._:-]+)?$" + "mount": { + "description": "Reference to a mounts[] entry by name", + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + } }, - - "volume_id": { - "description": "Volume identifier (hex string)", - "type": "string", - "pattern": "^[a-fA-F0-9]+$" - } + "required": ["name", "ip", "iscsi_initiator", "volume_id"], + "additionalProperties": false } }, "mounts": { @@ -108,83 +77,78 @@ }, "fs_type": { "type": "string", - "description": "Filesystem type (e.g., ext4, xfs, nfs, cifs, auto)", - "enum": ["auto", "ext2", "ext3", "ext4", "xfs", "btrfs", "nfs", "nfs4", "cifs", "tmpfs", "cephfs", "vfat", "ntfs"] + "description": "Filesystem type. Overrides mount_params profile when specified.", + "enum": ["auto", "ext2", "ext3", "ext4", "xfs", "btrfs", "nfs", "nfs4", "cifs", "tmpfs", "cephfs", "vfat", "ntfs", "none"] }, "mnt_opts": { "type": "string", - "description": "Mount options (e.g., defaults,noexec,nofail)", - "pattern": "^[a-zA-Z0-9,=._-]+$" + "description": "Mount options. Overrides mount_params profile when specified.", + "pattern": "^[a-zA-Z0-9,=._@/-]+$" }, "dump_freq": { "type": "string", - "description": "Dump frequency (usually 0)", + "description": "Dump frequency (usually 0). Overrides mount_params profile when specified.", "pattern": "^[0-2]$" }, "fsck_pass": { "type": "string", - "description": "Fsck pass number (usually 0 or 2)", + "description": "Fsck pass number (usually 0 or 2). Overrides mount_params profile when specified.", "pattern": "^[0-9]$" }, - "roles": { + "mount_params": { + "type": "string", + "description": "Name of the mount_params profile to use for unspecified fields", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + "functional_group_prefix": { "type": "array", - "description": "List of node roles to apply this mount to", + "description": "List of oChaMI functional group prefixes to apply this mount to. Omit to apply to all groups.", "items": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" }, "uniqueItems": true }, - "mount_default_field": { + "role": { "type": "string", - "description": "Name of the default profile to use for missing optional fields (references mount_default_fields.name)", - "pattern": "^[a-zA-Z0-9_-]+$" + "description": "Omnia-reserved role for this mount. Used for internal infrastructure mounts.", + "enum": ["omnia_slurm_share", "omnia_k8s_share"] } }, - "required": ["name", "source", "mount_point", "roles"], - "anyOf": [ - { - "required": ["mount_default_field"], - "description": "Must have mount_default_field to reference a default profile" - }, - { - "required": ["mnt_opts"], - "description": "Must have mnt_opts explicitly specified" - } - ], + "required": ["name", "source", "mount_point"], "additionalProperties": false } }, - "mount_default_fields": { + "mount_params": { "type": "object", - "description": "Named default mount configurations (key = profile name, value = profile settings)", + "description": "Named mount parameter profiles. Each profile provides defaults for fs_type, mnt_opts, dump_freq, fsck_pass. Custom fields are allowed for backend-specific metadata.", "patternProperties": { "^[a-zA-Z0-9_-]+$": { "type": "object", "properties": { "fs_type": { "type": "string", - "description": "Default filesystem type (e.g., ext4, xfs, nfs, cifs, auto)", + "description": "Default filesystem type", "enum": ["auto", "ext2", "ext3", "ext4", "xfs", "btrfs", "nfs", "nfs4", "cifs", "tmpfs", "cephfs", "vfat", "ntfs", "none"] }, "mnt_opts": { "type": "string", - "description": "Default mount options (e.g., defaults,noexec,nofail)", - "pattern": "^[a-zA-Z0-9,=._\\\\-]+$" + "description": "Default mount options", + "pattern": "^[a-zA-Z0-9,=._@/-]+$" }, "dump_freq": { "type": "string", - "description": "Default dump frequency (usually 0)", + "description": "Default dump frequency", "pattern": "^[0-2]$" }, "fsck_pass": { "type": "string", - "description": "Default fsck pass number (usually 0 or 2)", + "description": "Default fsck pass number", "pattern": "^[0-9]$" } }, "required": ["fs_type", "mnt_opts", "dump_freq", "fsck_pass"], - "additionalProperties": false + "additionalProperties": true } }, "additionalProperties": false @@ -214,12 +178,12 @@ }, "maxsize": { "type": "string", - "description": "Maximum size in bytes or human-readable format (used with size: auto)", + "description": "Maximum size (used with size: auto)", "pattern": "^[0-9]+[BKMGT]?$" }, - "roles": { + "functional_group_prefix": { "type": "array", - "description": "List of node roles to apply this swap to", + "description": "List of oChaMI functional group prefixes to apply this swap to. Omit to apply to all groups.", "items": { "type": "string", "pattern": "^[a-zA-Z0-9_-]+$" @@ -227,12 +191,11 @@ "uniqueItems": true } }, - "required": ["name", "filename", "size", "roles"], + "required": ["name", "filename", "size"], "additionalProperties": false } } }, - "required": [ - "nfs_client_params" - ] + "required": [], + "additionalProperties": false } diff --git a/input/omnia_config.yml b/input/omnia_config.yml index b0e9cb8850..d87fc62310 100644 --- a/input/omnia_config.yml +++ b/input/omnia_config.yml @@ -98,6 +98,9 @@ slurm_cluster: - cluster_name: slurm_cluster nfs_storage_name: nfs_slurm + mounts: # applicable to this cluster only + - nfs_slurm # Using nfs_slurm mount from storage_config.yml + - nfs_home # Using nfs_home mount from storage_config.yml # skip_merge: true # Uncomment to enable homogeneous discovery mode diff --git a/input/powervault_reason_runcmd.md b/input/powervault_reason_runcmd.md new file mode 100644 index 0000000000..ac8c4ed9f8 --- /dev/null +++ b/input/powervault_reason_runcmd.md @@ -0,0 +1,33 @@ +No. disk_setup cannot do this task. Here's why: + +What disk_setup + fs_setup can do + + • Partition a known, static block device path (e.g., /dev/sdb, /dev/sdc) + • Format the partition with a filesystem + • Works only on devices that exist at cloud-init boot time + +Why it cannot replace setup_iscsi_storage.sh + +The script does work that must happen before any device path is known: + +Step disk_setup capable? +Enable iscsid daemon No +Set /etc/iscsi/initiatorname.iscsi No +iSCSI target discovery (iscsiadm -m discovery) No +iSCSI login (iscsiadm -m node --login) No +Enable multipathd No +Identify the correct /dev/mapper/ by matching VOLUME_ID No +Create GPT partition if absent Yes (disk_setup) +Format with xfs if no filesystem exists Yes (fs_setup with overwrite: false) +Add fstab entry and mount Yes (mounts:) +Create bind mounts for mysql/spool subdirs Yes (mounts:) + +The hard blocker: disk_setup takes a static device path as the key (/dev/sdb, etc.). The multipath device (/dev/mapper/) is not known until after iSCSI login and multipath scanning — +which is dynamic runtime work that disk_setup has no hook for. + +The iSCSI/multipath setup steps (top 6 rows) must remain in runcmd or a script. Once the device is discovered and the path is known, the partition+format+mount portion could theoretically +use disk_setup+fs_setup+mounts: — but only if you hardcode /dev/mapper/ in the config, which defeats the purpose since volume_id matching is what identifies the right device +dynamically. + +Bottom line: disk_setup is for simple, pre-known local block devices. iSCSI over multipath requires daemon setup, target discovery, and dynamic device resolution — none of which cloud-init +modules support. The script stays in runcmd. \ No newline at end of file diff --git a/input/storage_config.yml b/input/storage_config.yml index d17cef561d..b21ed400bf 100644 --- a/input/storage_config.yml +++ b/input/storage_config.yml @@ -31,48 +31,31 @@ # Below is an example on how to configure powervault_config # In this configuration, a single controller portal is provided. -#powervault_config: -# ip: -# - 172.1.2.3 -# port: 3260 -# iscsi_initiator: iqn.2025-01.com.dell:scontrol-node -# volume_id: 00c0ff4343f1f1f1001c8c4e6901000000 +powervault_config: + - name: powervault1 + ip: + - 172.1.2.3 + port: 3260 + iscsi_initiator: iqn.2025-01.com.dell:scontrol-node + volume_id: 00c0ff4343f1f1f1001c8c4e6901000000 + # option A directly from cloud-init mounts module + mount: powervault_slurm_persist + + - name: powervault2 + ip: + - 172.1.2.4 + port: 3260 + iscsi_initiator: iqn.2025-01.com.dell:slurmd-node + volume_id: 00c0ff4343f1f1f1001c8c4e6901000001 + # option B - to directly specify all mount params + # source: "/dev/mapper/360002ac0000000000000000000000000" + # mount_point: "/scratch" + # mount_params: "vast_nfs_performance" + # fs_type: "xfs" + # mnt_opts: "defaults,nofail" + # functional_group_prefix: ["k8s_node"] -# -----------------------------NFS------------------------------------------------ - -# This variable is used for mounting NFS share on slurm_control_node, slurm_node, login_node -# This takes a list of dicts with possible keys server_ip, server_share_path, client_share_path, client_mount_options -# In both the cases, the USER must manually update 'server_ip' and 'server_share_path' below with the correct values. -# If mount_option values are empty, NFS client will be mounted with these values "nosuid,rw,sync,hard,intr" -# Its mandatory to provide atleast one entry in nfs_client_params -# Example for single mount file system: -# nfs_client_params: -# nfs_name : str ,Name of the NFS storage resource. The default is "nfs_storage_default". -# The user can assign any custom string to specify a different NFS storage resource. -# - { server_ip: 10.5.0.101, server_share_path: "/mnt/share", client_share_path: "/home", client_mount_options: "nosuid,rw,sync,hard"} -# Example for supporting multiple mount points: -# nfs_client_params: -# - { server_ip: 198.168.0.1,server_share_path: "/mnt/share1", client_share_path: "/home", client_mount_options: "nosuid,rw,sync,hard"} -# - { server_ip: 198.168.0.2, server_share_path: "/mnt/share2", client_share_path: "/mnt/mount2", client_mount_options: "nosuid,rw,sync,hard"} -# Example for multiple mount file system: -# nfs_client_params: -# - { server_ip: 198.168.0.1, server_share_path: "/mnt/share1", client_share_path: "/mnt/mount1", client_mount_options: "nosuid,rw,sync,hard"} -# - { server_ip: 198.168.0.2, server_share_path: "/mnt/share2", client_share_path: "/mnt/mount2", client_mount_options: "nosuid,rw,sync,hard"} -nfs_client_params: - - server_ip: "172.16.107.168" # Provide the IP of the NFS server - server_share_path: "/mnt/share/omnia" # Provide server share path of the NFS Server - client_share_path: /share_omnia - client_mount_options: "nosuid,rw,sync,hard,intr" - nfs_name: nfs_slurm - - - server_ip: "172.16.107.121" # Provide the IP of the NFS server - server_share_path: "/mnt/share/omnia_k8s" # Provide server share path of the NFS Server - client_share_path: /share_omnia_k8s - client_mount_options: "nosuid,rw,sync,hard,intr" - nfs_name: nfs_k8s - - # -----------------------------Cloud-Init Mounts------------------------------------------------ # mounts # Configure mount points and swap files compatible with cloud-init mounts module @@ -95,7 +78,9 @@ nfs_client_params: # - mount_default_field: Name of the default profile to use (references mount_default_fields.name). Optional # - Used ONLY for fields not explicitly specified in the mount entry # - If not specified, system will auto-select based on fs_type or use global defaults -# - roles: List of node roles to apply this mount to (e.g., ["slurm_control_node", "slurm_node"]). Required +# - functional_group_prefix: List of functional group prefixes to match against (e.g., ["slurm", "k8s", "login"]). Required +# - All nodes whose role starts with any of the given prefixes will have the mount applied +# - e.g., ["slurm"] matches slurm_control_node, slurm_node, etc. # # PRIORITY ORDER for field resolution: # 1. Explicit value in mount entry (HIGHEST PRIORITY) @@ -120,7 +105,8 @@ nfs_client_params: # - size: Size in bytes, 'auto', or human-readable format (e.g., "2G", "512M"). Required # Units: B, K, M, G, T (Note: 1K = 1024B) # - maxsize: Maximum size in bytes or human-readable format (used with size: auto). Optional -# - roles: List of node roles to apply this swap to (e.g., ["slurm_node"]). Required +# - functional_group_prefix: List of functional group prefixes to match against (e.g., ["slurm", "k8s"]). Required +# - All nodes whose role starts with any of the given prefixes will have the swap applied # Example configuration: # mounts: @@ -128,84 +114,96 @@ nfs_client_params: # - name: "powervault_slurm_persist" # source: "UUID=" # Or /dev/mapper/mpatha1 (partition created by setup script) # mount_point: "/mnt/slurm-persist" -# mount_default_field: "powervault_iscsi" +# mount_params: "powervault_iscsi" # # fs_type (xfs), mnt_opts, dump_freq, fsck_pass from powervault_iscsi profile -# roles: ["slurm_control_node"] +# functional_group_prefix: ["slurm_control_node"] # # # Example 2: PowerVault bind mount - MySQL data directory (from /mnt/slurm-persist/mysql) # - name: "powervault_mysql_bind" -# source: "/mnt/slurm-persist/mysql" -# mount_point: "/var/lib/mysql" -# mount_default_field: "bind_mounts" +# source: "/mnt/slurm-persist/" +# mount_point: "/all/slurm" +# mount_params: "bind_mounts" # # fs_type (none), mnt_opts (bind), dump_freq, fsck_pass from bind_mounts profile -# roles: ["slurm_control_node"] +# functional_group_prefix: ["slurm_control_node"] # # # Example 3: PowerVault bind mount - Slurm spool directory (from /mnt/slurm-persist/spool) # - name: "powervault_spool_bind" # source: "/mnt/slurm-persist/spool" # mount_point: "/var/spool" -# mount_default_field: "bind_mounts" +# mount_params: "bind_mounts" # # fs_type (none), mnt_opts (bind), dump_freq, fsck_pass from bind_mounts profile -# roles: ["slurm_control_node"] +# functional_group_prefix: ["k8s"] # # # Example 4: VAST NFS storage - Home directories using default profile # - name: "vast_home" # source: "192.168.1.100:/home" # VAST NFS export # mount_point: "/home" -# mount_default_field: "vast_nfs" +# mount_params: "vast_nfs" # # fs_type (nfs4), mnt_opts, dump_freq, fsck_pass from vast_nfs profile -# roles: ["slurm_control_node", "slurm_node", "login_node"] +# functional_group_prefix: ["slurm"] # # # Example 5: VAST NFS storage - Shared applications with explicit fs_type # - name: "vast_apps" # source: "192.168.1.100:/apps" # VAST NFS export # mount_point: "/opt/apps" # fs_type: "nfs4" # ← EXPLICIT (takes priority over profile) -# mount_default_field: "network_storage" +# mount_params: "network_storage" # # mnt_opts, dump_freq, fsck_pass from network_storage profile # # fs_type from network_storage is IGNORED (explicit wins) -# roles: ["slurm_control_node", "slurm_node"] +# functional_group_prefix: ["slurm"] # # # Example 6: VAST NFS storage - High-performance scratch with performance profile # - name: "vast_scratch" # source: "192.168.1.100:/scratch" # VAST NFS export # mount_point: "/scratch" -# mount_default_field: "vast_nfs_performance" +# mount_params: "vast_nfs_performance" # # fs_type (nfs4), mnt_opts (with 1MB buffers), dump_freq, fsck_pass from vast_nfs_performance profile -# roles: ["slurm_node"] +# functional_group_prefix: ["slurm_node"] + +mounts: + # Example 0: Atomic mount using all params + - name: Atomic mount + source: "UUID=" + mount_point: "/mnt/atomic" + fs_type: "nfs" + mnt_opts: "defaults,nofail,_netdev,x-systemd.after=cloud-init-network.service" + dump_freq: "0" + fsck_pass: "0" + # functional_group_prefix: # no mention will apply yo all mounts -mounts: [] # Example 1: PowerVault iSCSI storage - Persistent storage using profile - # - name: "powervault_slurm_persist" - # source: "UUID=" - # mount_point: "/mnt/slurm-persist" - # mount_default_field: "powervault_iscsi" - # # fs_type (xfs), mnt_opts, dump_freq, fsck_pass from powervault_iscsi profile - # roles: ["slurm_control_node"] - # + - name: "powervault_slurm_persist" + source: "UUID=" + mount_point: "/mnt/slurm-persist" + mount_params: "powervault_iscsi" + # fs_type (xfs), mnt_opts, dump_freq, fsck_pass from powervault_iscsi profile + functional_group_prefix: ["slurm_control_node_x86_64"] # This will only apply to slurm control nodes on x86_64 architecture + # Example 2: PowerVault bind mount - MySQL data directory - # - name: "powervault_mysql_bind" - # source: "/mnt/slurm-persist/mysql" - # mount_point: "/var/lib/mysql" - # mount_default_field: "bind_mounts" - # roles: ["slurm_control_node"] - # + - name: "powervault_mysql_bind" + source: "192.1.2.3:/mnt/mysql" + mount_point: "/var/lib/mysql" + mount_params: "nfs_bind_mounts" + functional_group_prefix: ["slurm_control_node"] # This will only apply to slurm control nodes all archs + # Example 3: VAST NFS storage - Home directories using VAST profile - # - name: "vast_home" - # source: "192.168.1.100:/home" - # mount_point: "/home" - # mount_default_field: "vast_nfs" - # roles: ["slurm_control_node", "slurm_node", "login_node"] - # + - name: "vast_home" + source: "{{ vast_nfs_ip }}:/home" + mount_point: "/home" + mount_params: "vast_nfs" + functional_group_prefix: ["slurm"] # This will only apply to all slurm nodes all archs + # Example 4: VAST NFS storage - High-performance scratch - # - name: "vast_scratch" - # source: "192.168.1.100:/scratch" - # mount_point: "/scratch" - # mount_default_field: "vast_nfs_performance" - # roles: ["slurm_node"] + - name: "vast_scratch" + source: "192.168.1.100:/scratch" + mount_point: "/scratch" + mount_params: "vast_nfs_performance" + functional_group_prefix: ["k8s_node"] # This will only apply to k8s nodes all archs # Named default profiles for mount configurations -mount_default_fields: +# Apart from fs_type, mnt_opts, dump_freq, fsck_pass others are custom fields, +# which can be used in mount templates +mount_params: # Default NFS mount configuration - standard NFS defaults default: fs_type: "nfs" @@ -219,6 +217,7 @@ mount_default_fields: mnt_opts: "defaults,nofail,_netdev,noatime,x-systemd.after=cloud-init-network.service" dump_freq: "0" fsck_pass: "0" + vast_nfs_ip: "192.168.1.100" # VAST NFS storage - High-performance configuration with large buffers vast_nfs_performance: @@ -226,6 +225,7 @@ mount_default_fields: mnt_opts: "defaults,nofail,_netdev,noatime,nodiratime,rsize=1048576,wsize=1048576" dump_freq: "0" fsck_pass: "0" + perf_ip: "192.168.1.101" # PowerVault iSCSI storage - Block device with XFS filesystem powervault_iscsi: @@ -275,18 +275,18 @@ mount_default_fields: # filename: "/swapfile" # size: "4G" # maxsize: "8G" -# roles: ["slurm_node"] +# functional_group_prefix: ["slurm_node"] # # - name: "k8s_swap" # filename: "/swapfile" # size: "2G" # maxsize: "4G" -# roles: ["kube_node"] +# functional_group_prefix: ["kube_node"] -swap: [] - # - name: "compute_swap" - # filename: "/swapfile" - # size: "2G" - # maxsize: "4G" - # roles: ["slurm_node"] +swap: + - name: "compute_swap" + filename: "/swapfile" + size: "2G" + maxsize: "4G" + functional_group_prefix: ["slurm_node"] diff --git a/input/storage_profile.yml b/input/storage_profile.yml new file mode 100644 index 0000000000..a0ddf78a43 --- /dev/null +++ b/input/storage_profile.yml @@ -0,0 +1,209 @@ +# ======================================== +# FINAL STORAGE CONFIGURATION +# ======================================== +# Purpose: Multi-cluster, multi-architecture storage mounting configuration +# Supports: Pure x86_64, pure aarch64, and hybrid clusters +# Storage Types: PowerScale (NFS), VAST (NFS/RDMA), PowerVault (iSCSI), External NFS + + + +storage_config: + # PowerVault Configuration - iSCSI Block Storage + powervault_config: + default_options: "defaults,_netdev,hard,intr,noatime" + # Volume 1 - Control Node Database (100GB) + pv1_volume1: + ip: ["10.10.0.21", "powervault01.cluster.local"] + port: 3260 + volume_id: "00c0ff4343f1f1f1001c8c4e6901000000" + iscsi_initiator: "iqn.2025-01.com.dell:scontrol-node" + options: "defaults,_netdev,hard,intr,noatime" + filesystem: "xfs" + + # Volume 2 - Control Node Backup (500GB) + pv1_volume2: + ip: ["10.10.0.21", "powervault01.cluster.local"] + port: 3260 + volume_id: "00c0ff4343f1f1f1001c8c4e6901000001" + iscsi_initiator: "iqn.2025-02.com.dell:scontrol-node" + filesystem: "xfs" + options: "defaults,_netdev,hard,intr,noatime" + + # PowerScale Configuration - NFS File Storage + powerscale_config: + default_options: "defaults,_netdev,hard,intr,noatime" + ps1: + protocol: "nfs" + ips: ["ps.cluster.local"] + options: "defaults,_netdev,hard,intr,noatime" + ps2: + ip: "ps.cluster.local" + protocol: "nfs" + options: "defaults,_netdev,hard,intr,noatime" + + # VAST Configuration - NFS/RDMA Parallel File Storage + vast_config: + default_options: "proto=rdma,port=20049,nconnect=8,remoteports=dns,mdconnect=2,spread_reads,spread_writes,noidlexprt,forcerdirplus" + vast1: + protocol: "nfs_rdma" + ips: ["vast.cluster.local"] + client_share_base_path: "/mnt/vast1" + server_share_base_path: "/mnt/share/omnia_vast1" + options: "proto=rdma,port=20049,nconnect=8,remoteports=dns,mdconnect=2,spread_reads,spread_writes,noidlexprt,forcerdirplus" + + # External NFS Configuration + nfs_config: + default_options: "defaults,_netdev,soft,intr" + nfs1: + protocol: "nfs" + ips: ["nfs.cluster.local"] + client_share_base_path: "/mnt/nfs1" + server_share_base_path: "/mnt/share/omnia_nfs1" + options: "defaults,_netdev,soft,intr" + nfs2: + protocol: "nfs" + ips: ["nfs.cluster.local"] + client_share_base_path: "/mnt/nfs1" + server_share_base_path: "/mnt/share/omnia_nfs1" + options: "defaults,_netdev,soft,intr" + +# ======================================== +# STORAGE PROFILES CONFIGURATION +# ======================================== +# Reusable mount profiles for different storage types and node roles + +storage_profiles: + slurm_common_profile: + slurm_control_nfs_profile: + ps1: + "/home": "/home" + "/data": "/data" + "/backup": "/backup" + "/var/slurm": "/slurm" + "/etc/slurm": "/etc/slurm" + "/var/log/slurm": "/var/log/slurm" + + slurm_control_mixed_profile: + ps1: + "/home": "/home" + "/data": "/data" + "/backup": "/backup" + "/var/slurm": "/slurm" + "/etc/slurm": "/etc/slurm" + "/var/log/slurm": "/var/log/slurm" + vast1: + "/opt/aarch64_tools": "/aarch64_tools" + "/lib/aarch64": "/lib_aarch64" + pv1_volume1: "/var/lib/mysql" + pv1_volume2: "/backup/object" + + # Slurm Login Node Profile - User Access + slurm_login_profile: + ps1: + "/home": "/home" + "/data": "/data" + "/shared": "/shared" + "/projects": "/projects" + "/tools": "/tools" + "/tmp": "/tmp" + + # Slurm Login/Compiler Node Profile - Development + Compilation + slurm_login_compiler_profile: + ps1: + "/home": "/home" + "/data": "/data" + "/shared": "/shared" + "/projects": "/projects" + "/tools": "/tools" + "/opt/compiler": "/compiler" + "/opt/build": "/build" + vast1: + "/scratch": "/scratch" + "/workspace": "/workspace" + "/tmp": "/tmp" + + + # Slurm Compute Node Profile - HPC Workloads + slurm_compute_profile: + ps1: + "/home": "/home" + "/data": "/data" + "/opt/apps": "/apps" + "/opt/hpc_tools": "/hpc_tools" + vast1: + "/scratch": "/scratch" + "/tmp": "/tmp" + "/workspace": "/workspace" + "/io-intensive": "/io-intensive" + + slurm_compute_profile_aarch64: + nfs2: + "/home": "/home" + "/data": "/data" + "/opt/apps": "/apps" + "/opt/hpc_tools": "/hpc_tools" + + # Kubernetes Worker Node Profile - Container Workloads + kube_node_profile: + ps2: + "/var/lib/containers": "/var/lib/containers" + "/var/lib/kubelet": "/var/lib/kubelet" + "/var/lib/podman": "/var/lib/podman" + "/opt/cni": "/opt/cni" + + # Kubernetes Controller Node Profile - Control Plane + kube_controller_profile: + ps2: + "/backup": "/backup" + "/etc/kubernetes": "/k8s-config" + "/etcd": "/etcd" + "/tmp": "/tmp" + "/var/lib/etcd": "/var/lib/etcd" + "/var/lib/kubelet": "/var/lib/kubelet" + "/var/lib/containers": "/var/lib/containers" + "/etc/certs": "/etc/certs" + "/opt/cni": "/opt/cni" + +# ======================================== +# SLURM MOUNTS CONFIGURATION +# ======================================== + +mounts: + slurm_cluster1: + # Control Nodes + slurm_control_x86_64: slurm_control_nfs_profile + slurm_control_aarch64: slurm_control_mixed_profile + + # Login Nodes + slurm_login_x86_64: + slurm_login_aarch64: slurm_login_profile + + # Login/Compiler Nodes + slurm_login_compiler_x86_64: slurm_login_compiler_profile + slurm_login_compiler_aarch64: slurm_login_compiler_profile + + # Compute Nodes + slurm_compute_x86_64: slurm_compute_profile + slurm_compute_aarch64: + - slurm_compute_profile + - slurm_compute_profile_aarch64 + + k8s_cluster1: + # Controller Nodes + kube_controller_x86_64: kube_controller_profile + + # Worker Nodes + kube_worker_x86_64: kube_node_profile + kube_worker_aarch64: kube_node_profile + +# ======================================== +# CONFIGURATION NOTES +# ======================================== +# Multi-storage-support example more that one pv, ps vast etc +# Multi-Cluster Support +# Multi-Architecture +# Complete Storage Types +# Mount Options +# Path Resolution Logic +# Volume Management +# Role Organization \ No newline at end of file From 46f6037f7d9a81ca33bf0257a3af7d6f2775cf60 Mon Sep 17 00:00:00 2001 From: Jagadeesh N V Date: Sun, 29 Mar 2026 02:08:33 +0530 Subject: [PATCH 09/10] storage design which looks good --- .../schema/storage_config.json | 2 +- input/storage_config.yml | 329 ++++++++++-------- 2 files changed, 178 insertions(+), 153 deletions(-) diff --git a/common/library/module_utils/input_validation/schema/storage_config.json b/common/library/module_utils/input_validation/schema/storage_config.json index a0dd9a37c0..eb6306515a 100644 --- a/common/library/module_utils/input_validation/schema/storage_config.json +++ b/common/library/module_utils/input_validation/schema/storage_config.json @@ -147,7 +147,7 @@ "pattern": "^[0-9]$" } }, - "required": ["fs_type", "mnt_opts", "dump_freq", "fsck_pass"], + "required": ["fs_type", "mnt_opts"], "additionalProperties": true } }, diff --git a/input/storage_config.yml b/input/storage_config.yml index b21ed400bf..39c9595f94 100644 --- a/input/storage_config.yml +++ b/input/storage_config.yml @@ -19,17 +19,47 @@ # -----------------------------Powervault------------------------------------------- # powervault_config -# Mandatory when using PowerVault for persistent storage. -# Below parameters are mandatory when powervault_config is defined - # ip: A list of PowerVault controller ipv4 addresses used for iSCSI target discovery and login. - # iscsi_initiator: Specifies the InitiatorName used by the host when connecting to the iSCSI target. This IQN uniquely identifies the host to the storage array. - # volume_id: This is the unique WWN/identifier for the specific volume that should be used for persistent storage. This value is used for multipath scanning to select the correct mapped device. - -# Below are the optional parameters when powervault_config is defined - # port: Defines the TCP port for the iSCSI target service. When port is not specified, default port used will be 3260 +# Processed entirely via runcmd script (setup_iscsi_storage.sh). +# The device path (/dev/mapper/XXX) is only known after iSCSI login + multipath scan, +# so powervault mounts CANNOT use the cloud-init mounts module. +# The runcmd script handles: iscsid enable, initiator name, discovery, login, +# multipathd, volume_id matching, partitioning, formatting, mount, and bind mounts. +# +# Mandatory parameters: +# - name: Unique identifier for this powervault entry. Required +# - ip: List of PowerVault controller IPv4 addresses for iSCSI target discovery. Required +# - iscsi_initiator: InitiatorName IQN for the host. Required +# - volume_id: WWN/identifier for the volume (used for multipath device matching). Required +# +# Optional parameters: +# - port: TCP port for iSCSI target service. Default: 3260 +# - mount_point: Where the discovered device gets mounted. Required +# - mount_params: Named profile for fs_type/mnt_opts (read by the runcmd script). Optional +# - node_key: ds.meta_data key for per-node bind mounts (e.g., "local_hostname"). Optional +# - When present, implies bind mount: // -> +# - fs_type forced to "none", mnt_opts forced to "bind" (automatic) +# - node_mount_point: List of bind mount targets. Required when node_key is set +# - Pattern: // -> +# - slurm_conf_var: List of slurm.conf dir parameters to set to the union of node_mount_point values. Optional +# - Only directory params: StateSaveLocation, SlurmdSpoolDir +# - functional_group_prefix: List of functional group prefixes for node targeting. Required -# Below is an example on how to configure powervault_config -# In this configuration, a single controller portal is provided. +# Example: +# powervault_config: +# - name: powervault1 +# ip: [172.1.2.3] +# port: 3260 +# iscsi_initiator: iqn.2025-01.com.dell:scontrol-node +# volume_id: 00c0ff4343f1f1f1001c8c4e6901000000 +# mount_point: "/mnt/slurm-persist" +# mount_params: "powervault_iscsi" +# node_key: "local_hostname" +# node_mount_point: +# - "/var/lib/mysql" +# - "/var/spool/slurm" +# slurm_conf_var: +# - "StateSaveLocation" +# functional_group_prefix: ["slurm_control_node"] powervault_config: - name: powervault1 @@ -38,202 +68,203 @@ powervault_config: port: 3260 iscsi_initiator: iqn.2025-01.com.dell:scontrol-node volume_id: 00c0ff4343f1f1f1001c8c4e6901000000 - # option A directly from cloud-init mounts module - mount: powervault_slurm_persist - + mount_point: "/mnt/slurm-persist" + mount_params: "powervault_iscsi" + node_key: "local_hostname" + node_mount_point: + - "/var/lib/mysql" + - "/var/spool/slurm" + slurm_conf_var: + - "StateSaveLocation" + functional_group_prefix: ["slurm_control_node"] + - name: powervault2 ip: - 172.1.2.4 port: 3260 iscsi_initiator: iqn.2025-01.com.dell:slurmd-node volume_id: 00c0ff4343f1f1f1001c8c4e6901000001 - # option B - to directly specify all mount params - # source: "/dev/mapper/360002ac0000000000000000000000000" - # mount_point: "/scratch" - # mount_params: "vast_nfs_performance" - # fs_type: "xfs" - # mnt_opts: "defaults,nofail" - # functional_group_prefix: ["k8s_node"] + mount_point: "/mnt/slurmd-persist" + mount_params: "powervault_iscsi" + functional_group_prefix: ["slurm_node"] + +#TODO: for powervault one more possibility is is to use one mount with source like powervault:powervault2 # -----------------------------Cloud-Init Mounts------------------------------------------------ # mounts -# Configure mount points and swap files compatible with cloud-init mounts module -# This configuration will be used to generate cloud-init compatible mount configurations - +# Configure mount points compatible with cloud-init mounts module. +# Source must be known at boot time (NFS paths, UUIDs, local devices). +# For runtime-discovered sources (iSCSI/multipath), use powervault_config above. +# # Each mount entry contains the following fields (matching /etc/fstab format): -# - name: Unique identifier for this mount entry (e.g., "iscsi_slurm_persist", "nfs_slurm_home"). Required -# - source: Device name (e.g., /dev/sdc, UUID=xxx, or network path like 192.168.1.100:/export/share). Required +# - name: Unique identifier for this mount entry. Required +# - source: Device or network path (e.g., /dev/sdc, UUID=xxx, 192.168.1.100:/share). Required # - mount_point: Mount point path (e.g., /mnt, /opt/data). Required -# - fs_type: Filesystem type (e.g., ext4, xfs, nfs, cifs, auto). Optional -# - Supported: nfs, nfs4, cifs, tmpfs, cephfs, btrfs, ext2, ext3, ext4, xfs, none, etc. -# - For network filesystems (nfs, cifs, etc.), source should be the server path (e.g., 192.168.1.100:/export/share) -# - If specified, takes PRIORITY over mount_default_field +# - fs_type: Filesystem type (e.g., ext4, xfs, nfs, nfs4, cifs, auto). Optional +# - If specified, takes PRIORITY over mount_params profile # - mnt_opts: Mount options (e.g., defaults,noexec,nofail). Optional -# - If specified, takes PRIORITY over mount_default_field +# - If specified, takes PRIORITY over mount_params profile # - dump_freq: Dump frequency (usually "0"). Optional -# - If specified, takes PRIORITY over mount_default_field +# - If specified, takes PRIORITY over mount_params profile # - fsck_pass: Fsck pass number (usually "0" or "2"). Optional -# - If specified, takes PRIORITY over mount_default_field -# - mount_default_field: Name of the default profile to use (references mount_default_fields.name). Optional +# - If specified, takes PRIORITY over mount_params profile +# - mount_params: Name of a profile in mount_params section. Optional # - Used ONLY for fields not explicitly specified in the mount entry -# - If not specified, system will auto-select based on fs_type or use global defaults -# - functional_group_prefix: List of functional group prefixes to match against (e.g., ["slurm", "k8s", "login"]). Required -# - All nodes whose role starts with any of the given prefixes will have the mount applied +# - node_key: ds.meta_data key for per-node bind mounts (e.g., "local_hostname", "local_ipv4"). Optional +# - When present, implies bind mount with per-node source path +# - fs_type forced to "none", mnt_opts forced to "bind" (automatic) +# - Source becomes: // +# - node_mount_point: List of bind mount targets. Required when node_key is set +# - Each target gets: // -> +# - slurm_conf_var: List of slurm.conf dir parameters. Optional +# - Each param gets the union of all node_mount_point values +# - Only directory params: StateSaveLocation, SlurmdSpoolDir +# - functional_group_prefix: List of functional group prefixes. Optional +# - All nodes whose role starts with any prefix get this mount # - e.g., ["slurm"] matches slurm_control_node, slurm_node, etc. +# - If omitted, mount applies to all nodes # # PRIORITY ORDER for field resolution: # 1. Explicit value in mount entry (HIGHEST PRIORITY) -# 2. Value from mount_default_field profile (if specified) +# 2. Value from mount_params profile (if specified) # 3. Auto-selected profile based on fs_type # 4. Global fallback profile # 5. Hardcoded system defaults (LOWEST PRIORITY) -# mount_default_fields: Mapping of named default profiles (key = profile name) -# Each profile (value) contains: -# - fs_type: Default filesystem type (e.g., "auto", "xfs", "nfs"). Required -# - mnt_opts: Default mount options (e.g., "defaults,nofail,_netdev"). Required -# - dump_freq: Default dump frequency (usually "0"). Required -# - fsck_pass: Default fsck pass number (usually "0" or "2"). Required -# Note: Profiles are templates that can be referenced by any mount entry -# Note: Profile names must match pattern ^[a-zA-Z0-9_-]+$ (alphanumeric, underscore, hyphen) - -# swap: Swap file configuration (list of swap configurations) -# Each swap entry contains the following fields: -# - name: Unique identifier for this swap entry (e.g., "compute_swap", "slurm_swap"). Required -# - filename: Path to the swap file to create (e.g., /swapfile). Required -# - size: Size in bytes, 'auto', or human-readable format (e.g., "2G", "512M"). Required -# Units: B, K, M, G, T (Note: 1K = 1024B) -# - maxsize: Maximum size in bytes or human-readable format (used with size: auto). Optional -# - functional_group_prefix: List of functional group prefixes to match against (e.g., ["slurm", "k8s"]). Required -# - All nodes whose role starts with any of the given prefixes will have the swap applied - -# Example configuration: +# Example: static mount with all explicit params (no profile) # mounts: -# # Example 1: PowerVault iSCSI storage - Persistent storage (matches setup_iscsi_storage.sh) -# - name: "powervault_slurm_persist" -# source: "UUID=" # Or /dev/mapper/mpatha1 (partition created by setup script) -# mount_point: "/mnt/slurm-persist" -# mount_params: "powervault_iscsi" -# # fs_type (xfs), mnt_opts, dump_freq, fsck_pass from powervault_iscsi profile -# functional_group_prefix: ["slurm_control_node"] +# - name: "atomic_mount" +# source: "192.168.1.100:/export" +# mount_point: "/mnt/data" +# fs_type: "nfs4" +# mnt_opts: "nfsvers=4.1,hard,intr,noatime,nconnect=16,rsize=1048576,wsize=1048576" +# dump_freq: "0" +# fsck_pass: "0" # -# # Example 2: PowerVault bind mount - MySQL data directory (from /mnt/slurm-persist/mysql) -# - name: "powervault_mysql_bind" -# source: "/mnt/slurm-persist/" -# mount_point: "/all/slurm" -# mount_params: "bind_mounts" -# # fs_type (none), mnt_opts (bind), dump_freq, fsck_pass from bind_mounts profile -# functional_group_prefix: ["slurm_control_node"] -# -# # Example 3: PowerVault bind mount - Slurm spool directory (from /mnt/slurm-persist/spool) -# - name: "powervault_spool_bind" -# source: "/mnt/slurm-persist/spool" -# mount_point: "/var/spool" -# mount_params: "bind_mounts" -# # fs_type (none), mnt_opts (bind), dump_freq, fsck_pass from bind_mounts profile -# functional_group_prefix: ["k8s"] -# -# # Example 4: VAST NFS storage - Home directories using default profile +# Example: static mount using profile # - name: "vast_home" -# source: "192.168.1.100:/home" # VAST NFS export +# source: "192.168.1.100:/home" # mount_point: "/home" # mount_params: "vast_nfs" -# # fs_type (nfs4), mnt_opts, dump_freq, fsck_pass from vast_nfs profile -# functional_group_prefix: ["slurm"] -# -# # Example 5: VAST NFS storage - Shared applications with explicit fs_type -# - name: "vast_apps" -# source: "192.168.1.100:/apps" # VAST NFS export -# mount_point: "/opt/apps" -# fs_type: "nfs4" # ← EXPLICIT (takes priority over profile) -# mount_params: "network_storage" -# # mnt_opts, dump_freq, fsck_pass from network_storage profile -# # fs_type from network_storage is IGNORED (explicit wins) # functional_group_prefix: ["slurm"] # -# # Example 6: VAST NFS storage - High-performance scratch with performance profile -# - name: "vast_scratch" -# source: "192.168.1.100:/scratch" # VAST NFS export -# mount_point: "/scratch" -# mount_params: "vast_nfs_performance" -# # fs_type (nfs4), mnt_opts (with 1MB buffers), dump_freq, fsck_pass from vast_nfs_performance profile +# Example: per-node bind mount (node_key triggers bind behavior) +# - name: "scratch_isolation" +# source: "/mnt/scratch" +# node_key: "local_hostname" +# node_mount_point: +# - "/scratch" +# - "/tmp" +# slurm_conf_var: +# - "SlurmdSpoolDir" # functional_group_prefix: ["slurm_node"] +# # On node001 generates fstab: +# # /mnt/scratch/node001/scratch /scratch none bind 0 0 +# # /mnt/scratch/node001/tmp /tmp none bind 0 0 +# # slurm.conf: SlurmdSpoolDir=/scratch,/tmp mounts: - # Example 0: Atomic mount using all params - - name: Atomic mount + # Static mount: all explicit params, no profile, applies to all nodes + - name: "atomic_mount" source: "UUID=" mount_point: "/mnt/atomic" fs_type: "nfs" mnt_opts: "defaults,nofail,_netdev,x-systemd.after=cloud-init-network.service" dump_freq: "0" fsck_pass: "0" - # functional_group_prefix: # no mention will apply yo all mounts - # Example 1: PowerVault iSCSI storage - Persistent storage using profile - - name: "powervault_slurm_persist" - source: "UUID=" - mount_point: "/mnt/slurm-persist" - mount_params: "powervault_iscsi" - # fs_type (xfs), mnt_opts, dump_freq, fsck_pass from powervault_iscsi profile - functional_group_prefix: ["slurm_control_node_x86_64"] # This will only apply to slurm control nodes on x86_64 architecture - - # Example 2: PowerVault bind mount - MySQL data directory - - name: "powervault_mysql_bind" - source: "192.1.2.3:/mnt/mysql" - mount_point: "/var/lib/mysql" - mount_params: "nfs_bind_mounts" - functional_group_prefix: ["slurm_control_node"] # This will only apply to slurm control nodes all archs - - # Example 3: VAST NFS storage - Home directories using VAST profile + # VAST NFS: shared export for home directories - name: "vast_home" source: "{{ vast_nfs_ip }}:/home" mount_point: "/home" mount_params: "vast_nfs" - functional_group_prefix: ["slurm"] # This will only apply to all slurm nodes all archs - - # Example 4: VAST NFS storage - High-performance scratch - - name: "vast_scratch" + functional_group_prefix: ["slurm"] + + # VAST NFS: shared export for applications + - name: "vast_apps" + source: "{{ vast_nfs_ip }}:/apps" + mount_point: "/apps" + mount_params: "vast_nfs" + functional_group_prefix: ["slurm"] + + # VAST NFS: shared export for slurm state (controller) + - name: "vast_slurm_state" + source: "{{ vast_nfs_ip }}:/slurm" + mount_point: "/var/slurm" + mount_params: "vast_nfs" + slurm_conf_var: + - "StateSaveLocation" + functional_group_prefix: ["slurm_control_node"] + + # VAST NFS: shared scratch export + - name: "vast_scratch_shared" source: "192.168.1.100:/scratch" - mount_point: "/scratch" + mount_point: "/mnt/scratch" mount_params: "vast_nfs_performance" - functional_group_prefix: ["k8s_node"] # This will only apply to k8s nodes all archs + functional_group_prefix: ["slurm_node"] + + # Per-node bind mount from shared scratch (node_key triggers bind behavior) + - name: "scratch_isolation" + source: "/mnt/scratch" + mount_point: "/mounted/scratch" + node_key: "local_hostname" + node_mount_point: + - "/scratch" + - "/tmp" + slurm_conf_var: + - "SlurmdSpoolDir" + functional_group_prefix: ["slurm_node"] + + # Powervault: shared export for slurm state (controller) + - name: "powervault_slurm_state" + source: "powervault:powervault2" + mount_point: "/mnt/slurmd-persist" + mount_params: "powervault_iscsi" + functional_group_prefix: ["slurm_node"] + +# -----------------------------Mount Params (Profiles)------------------------------- +# Named default profiles for mount configurations. +# Apart from fs_type, mnt_opts, dump_freq, fsck_pass, additional custom fields +# can be defined and used in mount templates (e.g., vast_nfs_ip). -# Named default profiles for mount configurations -# Apart from fs_type, mnt_opts, dump_freq, fsck_pass others are custom fields, -# which can be used in mount templates mount_params: - # Default NFS mount configuration - standard NFS defaults + # Default NFS mount - standard NFS4.1 with high-performance options default: - fs_type: "nfs" - mnt_opts: "defaults,nofail,_netdev,x-systemd.after=cloud-init-network.service" + fs_type: "nfs4" + mnt_opts: "nfsvers=4.1,hard,intr,noatime,nconnect=16,rsize=1048576,wsize=1048576" dump_freq: "0" fsck_pass: "0" - # VAST NFS storage - Standard configuration for VAST Data NFS exports + # VAST NFS storage - standard configuration vast_nfs: - fs_type: "nfs4" - mnt_opts: "defaults,nofail,_netdev,noatime,x-systemd.after=cloud-init-network.service" + fs_type: "nfs" + mnt_opts: "nfsvers=3,hard,intr,noatime,nconnect=16,rsize=1048576,wsize=1048576" dump_freq: "0" fsck_pass: "0" vast_nfs_ip: "192.168.1.100" - # VAST NFS storage - High-performance configuration with large buffers + # VAST NFS storage - high-performance with large buffers vast_nfs_performance: - fs_type: "nfs4" - mnt_opts: "defaults,nofail,_netdev,noatime,nodiratime,rsize=1048576,wsize=1048576" + fs_type: "nfs" + mnt_opts: "nfsvers=3,hard,intr,noatime,nodiratime,nconnect=16,rsize=1048576,wsize=1048576" dump_freq: "0" fsck_pass: "0" - perf_ip: "192.168.1.101" - # PowerVault iSCSI storage - Block device with XFS filesystem + # PowerVault iSCSI storage - block device with XFS powervault_iscsi: fs_type: "xfs" mnt_opts: "defaults,_netdev,noatime,x-systemd.requires=iscsi.service" dump_freq: "0" fsck_pass: "0" + # BeeGFS parallel filesystem + beegfs: + fs_type: "beegfs" + mnt_opts: "cfgFile=/etc/beegfs/beegfs-client.conf" + dump_freq: "0" + fsck_pass: "0" + # Network storage defaults (generic NFS, CIFS, etc.) network_storage: fs_type: "auto" @@ -269,19 +300,14 @@ mount_params: dump_freq: "0" fsck_pass: "2" -# Example swap configuration: -# swap: -# - name: "slurm_swap" -# filename: "/swapfile" -# size: "4G" -# maxsize: "8G" -# functional_group_prefix: ["slurm_node"] -# -# - name: "k8s_swap" -# filename: "/swapfile" -# size: "2G" -# maxsize: "4G" -# functional_group_prefix: ["kube_node"] +# -----------------------------Swap------------------------------------------------- +# swap: Swap file configuration (list of swap configurations) +# Each swap entry contains: +# - name: Unique identifier. Required +# - filename: Path to the swap file (e.g., /swapfile). Required +# - size: Size in bytes, 'auto', or human-readable (e.g., "2G", "512M"). Required +# - maxsize: Max size (used with size: auto). Optional +# - functional_group_prefix: List of functional group prefixes. Required swap: - name: "compute_swap" @@ -289,4 +315,3 @@ swap: size: "2G" maxsize: "4G" functional_group_prefix: ["slurm_node"] - From 4897f7b490f14e7afde465b3523cd6997eefb56c Mon Sep 17 00:00:00 2001 From: Jagadeesh N V Date: Tue, 31 Mar 2026 17:09:17 +0530 Subject: [PATCH 10/10] Storage design final --- STORAGE_DESIGN.md | 60 ++++++++- .../schema/storage_config.json | 114 ++++++++++++++++-- .../ci-group-slurm_node_aarch64.yaml.j2 | 8 +- input/storage_config.yml | 47 ++++++-- 4 files changed, 205 insertions(+), 24 deletions(-) diff --git a/STORAGE_DESIGN.md b/STORAGE_DESIGN.md index bad3a2e94f..a7e9666931 100644 --- a/STORAGE_DESIGN.md +++ b/STORAGE_DESIGN.md @@ -16,8 +16,9 @@ 6. [Usage Examples](#usage-examples) 7. [Priority Resolution](#priority-resolution) 8. [Best Practices](#best-practices) -9. [Validation Rules](#validation-rules) -10. [Design Decision: storage_config.yml vs storage_profile.yml](#design-decision-storage_configyml-vs-storage_profileyml) +9. [Multi-Storage System Support](#multi-storage-system-support) +10. [Validation Rules](#validation-rules) +11. [Design Decision: storage_config.yml vs storage_profile.yml](#design-decision-storage_configyml-vs-storage_profileyml) --- @@ -826,6 +827,61 @@ mnt_opts: "defaults,_netdev,noatime,nobarrier,x-systemd.requires=iscsi.service" --- +## Multi-Storage System Support + +The design is **storage-system and access-method agnostic** by construction. It is a mount model, not a storage-vendor model — it doesn't care what the storage system is, only whether the source is known at boot time (`mounts:`) or runtime-discovered (`powervault_config:` / runcmd). + +### Coverage Matrix + +| Storage System | Access Method | Mechanism | Profile | +|---|---|---|---| +| **PowerVault** | iSCSI/multipath | `powervault_config:` → runcmd (runtime discovery) | `powervault_iscsi` | +| **VAST** | NFS | `mounts:` + static source | `vast_nfs` / `vast_nfs_performance` | +| **VAST** | RDMA | `mounts:` + `mount_params` profile with RDMA opts | custom profile | +| **PowerScale** | NFS | `mounts:` entry — just a different source IP | `powerscale_nfs` (user-defined) | +| **PowerScale** | SMB/CIFS | `mounts:` with `fs_type: cifs` + cred opts | custom profile | +| **BeeGFS** | FUSE/RDMA | `mounts:` with BeeGFS client mount | `beegfs` | +| **Any NFS server** | NFS3/NFS4 | `mounts:` + profile or explicit `fs_type`/`mnt_opts` | `default` / `network_storage` | +| **S3** | s3fs-fuse | `mounts:` with `fs_type: fuse.s3fs` + cred opts | custom profile | +| **Local disk** | block device | `mounts:` with UUID/device path | `local_storage` / `scratch_storage` | + +### Why This Works + +1. **`mounts:` is protocol-agnostic.** `source` is just a string (IP:/path, UUID, device path, s3 bucket). `fs_type` and `mnt_opts` handle the protocol specifics. + +2. **`mount_params:` profiles absorb vendor differences.** Each storage+protocol combo is a profile. Users reference the profile name, don't think about options. + +3. **`powervault_config:` exists only because iSCSI needs runtime discovery.** Any storage needing runtime device resolution before mount follows the same pattern (runcmd-based). Everything with a known source at config time fits in `mounts:`. + +4. **Custom fields in profiles carry vendor-specific variables.** `vast_nfs_ip` in the `vast_nfs` profile demonstrates this. Same pattern works for PowerScale VIPs, BeeGFS mgmtd hosts, etc. + +### Adding a New Storage System (Zero Schema Changes) + +Example: PowerScale NFS — define a profile and reference it. + +```yaml +mount_params: + powerscale_nfs: + fs_type: "nfs4" + mnt_opts: "nfsvers=4.1,hard,intr,noatime,nconnect=16,rsize=1048576,wsize=1048576" + dump_freq: "0" + fsck_pass: "0" + powerscale_ip: "10.0.1.50" # Custom field — available to Jinja2 templates + +mounts: + - name: "powerscale_home" + source: "{{ powerscale_ip }}:/ifs/home" + mount_point: "/home" + mount_params: "powerscale_nfs" + functional_group_prefix: ["slurm"] +``` + +### When a New Top-Level Section Is Needed + +Only when storage requires **runtime device discovery before mount** — where the source path can't be known at boot time. Currently that's just iSCSI/multipath (`powervault_config:`). Everything else (NFS, CIFS, s3fs, BeeGFS, VAST RDMA client) has a known source at config time and fits in `mounts:`. + +--- + ## Validation Rules ### Required Fields diff --git a/common/library/module_utils/input_validation/schema/storage_config.json b/common/library/module_utils/input_validation/schema/storage_config.json index eb6306515a..b86b843f7e 100644 --- a/common/library/module_utils/input_validation/schema/storage_config.json +++ b/common/library/module_utils/input_validation/schema/storage_config.json @@ -5,7 +5,7 @@ "properties": { "powervault_config": { "type": "array", - "description": "List of PowerVault iSCSI volume connection definitions", + "description": "List of PowerVault iSCSI volume connection definitions. Processed via runcmd script because device path is only known after iSCSI login + multipath scan.", "items": { "type": "object", "properties": { @@ -17,7 +17,7 @@ "maxLength": 64 }, "ip": { - "description": "List of target controller IP addresses", + "description": "List of target controller IP addresses for iSCSI discovery", "type": "array", "minItems": 1, "items": { @@ -38,23 +38,102 @@ "pattern": "^iqn\\.[a-zA-Z0-9.-]+(?::[a-zA-Z0-9._:-]+)?$" }, "volume_id": { - "description": "Volume identifier (hex string / WWN)", + "description": "Volume identifier (hex string / WWN) for multipath device matching", "type": "string", "pattern": "^[a-fA-F0-9]+$" }, - "mount": { - "description": "Reference to a mounts[] entry by name", + "mount_point": { + "type": "string", + "description": "Where the discovered device gets mounted", + "pattern": "^/[a-zA-Z0-9/_.-]*$" + }, + "mount_params": { "type": "string", + "description": "Named profile for fs_type/mnt_opts (read by the runcmd script)", "pattern": "^[a-zA-Z0-9_-]+$" + }, + "node_key": { + "type": "string", + "description": "ds.meta_data key for per-node bind mounts (e.g., local_hostname). When present, generates bind mounts under mount_point//", + "enum": ["local_hostname", "local_ipv4", "instance_id"] + }, + "node_mount_point": { + "type": "array", + "description": "List of bind mount target paths. Required when node_key is set. Each gets: mount_point// -> ", + "items": { + "type": "string", + "pattern": "^/[a-zA-Z0-9/_.-]*$" + }, + "minItems": 1, + "uniqueItems": true + }, + + "functional_group_prefix": { + "type": "array", + "description": "List of functional group prefixes for node targeting", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + "uniqueItems": true + } + }, + "required": ["name", "ip", "iscsi_initiator", "volume_id", "mount_point", "functional_group_prefix"], + "additionalProperties": false + } + }, + "beegfs_config": { + "type": "array", + "description": "List of BeeGFS parallel filesystem mount definitions. BeeGFS uses beegfs-mounts.conf and beegfs-client service, not fstab. Each entry represents one BeeGFS cluster connection.", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Unique identifier for this BeeGFS mount", + "pattern": "^[a-zA-Z0-9_-]+$", + "minLength": 1, + "maxLength": 64 + }, + "mgmtd_host": { + "type": "string", + "description": "IP address of the BeeGFS management server", + "format": "ipv4" + }, + "mount_point": { + "type": "string", + "description": "Client mount location for this BeeGFS filesystem", + "pattern": "^/[a-zA-Z0-9/_.-]*$" + }, + "conn_auth_file": { + "type": "string", + "description": "Path to the BeeGFS connauth shared secret file on the omnia_core. Copied to /etc/beegfs/ on target nodes.", + "pattern": "^/[a-zA-Z0-9/_.-]*$" + }, + "client_conf_overrides": { + "type": "object", + "description": "Optional key-value overrides for /etc/beegfs/beegfs-client.conf (e.g., tuneNumWorkers, connMaxInternodeNum)", + "additionalProperties": { + "type": ["string", "integer", "boolean"] + } + }, + "functional_group_prefix": { + "type": "array", + "description": "List of functional group prefixes for node targeting", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$" + }, + "uniqueItems": true } }, - "required": ["name", "ip", "iscsi_initiator", "volume_id"], + "required": ["name", "mgmtd_host", "mount_point", "conn_auth_file", "functional_group_prefix"], "additionalProperties": false } }, "mounts": { "type": "array", - "description": "Cloud-init compatible mount configurations", + "description": "Cloud-init compatible mount configurations. Source must be known at boot time.", "items": { "type": "object", "properties": { @@ -67,7 +146,7 @@ }, "source": { "type": "string", - "description": "Device name or network path (e.g., /dev/sdc, UUID=xxx, 192.168.1.100:/export/share)", + "description": "Device name or network path (e.g., /dev/sdc, UUID=xxx, 192.168.1.100:/export/share, powervault:)", "minLength": 1 }, "mount_point": { @@ -78,7 +157,7 @@ "fs_type": { "type": "string", "description": "Filesystem type. Overrides mount_params profile when specified.", - "enum": ["auto", "ext2", "ext3", "ext4", "xfs", "btrfs", "nfs", "nfs4", "cifs", "tmpfs", "cephfs", "vfat", "ntfs", "none"] + "enum": ["auto", "ext2", "ext3", "ext4", "xfs", "btrfs", "nfs", "nfs4", "cifs", "tmpfs", "cephfs", "vfat", "ntfs", "none", "beegfs", "fuse.s3fs"] }, "mnt_opts": { "type": "string", @@ -100,6 +179,21 @@ "description": "Name of the mount_params profile to use for unspecified fields", "pattern": "^[a-zA-Z0-9_-]+$" }, + "node_key": { + "type": "string", + "description": "ds.meta_data key for per-node bind mounts. When present, fs_type is forced to none and mnt_opts to bind.", + "enum": ["local_hostname", "local_ipv4", "instance_id"] + }, + "node_mount_point": { + "type": "array", + "description": "List of bind mount target paths. Required when node_key is set.", + "items": { + "type": "string", + "pattern": "^/[a-zA-Z0-9/_.-]*$" + }, + "minItems": 1, + "uniqueItems": true + }, "functional_group_prefix": { "type": "array", "description": "List of oChaMI functional group prefixes to apply this mount to. Omit to apply to all groups.", @@ -129,7 +223,7 @@ "fs_type": { "type": "string", "description": "Default filesystem type", - "enum": ["auto", "ext2", "ext3", "ext4", "xfs", "btrfs", "nfs", "nfs4", "cifs", "tmpfs", "cephfs", "vfat", "ntfs", "none"] + "enum": ["auto", "ext2", "ext3", "ext4", "xfs", "btrfs", "nfs", "nfs4", "cifs", "tmpfs", "cephfs", "vfat", "ntfs", "none", "beegfs", "fuse.s3fs"] }, "mnt_opts": { "type": "string", diff --git a/discovery/roles/configure_ochami/templates/cloud_init/ci-group-slurm_node_aarch64.yaml.j2 b/discovery/roles/configure_ochami/templates/cloud_init/ci-group-slurm_node_aarch64.yaml.j2 index f2be7fa3c8..1f3e02429a 100644 --- a/discovery/roles/configure_ochami/templates/cloud_init/ci-group-slurm_node_aarch64.yaml.j2 +++ b/discovery/roles/configure_ochami/templates/cloud_init/ci-group-slurm_node_aarch64.yaml.j2 @@ -321,6 +321,12 @@ echo "[INFO] ===== Completed slurmd setup (aarch64) =====" + - path: /etc/munge/munge.key + owner: {{ munge_user }}:{{ munge_group }} + permissions: '{{ file_mode_400 }}' + encoding: b64 + content: {{ slurp_munge_key.content }} + - path: /usr/local/bin/configure_munge_and_pam.sh permissions: '{{ file_mode_755 }}' content: | @@ -442,7 +448,7 @@ content: | {% for key in ip_name_map | sort %} {{ ip_name_map[key] }} {{ key }} -{% endfor %} +{%- endfor %} - path: /etc/sysconfig/slurmd owner: root:root diff --git a/input/storage_config.yml b/input/storage_config.yml index 39c9595f94..bd71898b97 100644 --- a/input/storage_config.yml +++ b/input/storage_config.yml @@ -53,7 +53,7 @@ # volume_id: 00c0ff4343f1f1f1001c8c4e6901000000 # mount_point: "/mnt/slurm-persist" # mount_params: "powervault_iscsi" -# node_key: "local_hostname" +# node_key: "local_ipv4" # node_mount_point: # - "/var/lib/mysql" # - "/var/spool/slurm" @@ -68,14 +68,13 @@ powervault_config: port: 3260 iscsi_initiator: iqn.2025-01.com.dell:scontrol-node volume_id: 00c0ff4343f1f1f1001c8c4e6901000000 + # mount params mount_point: "/mnt/slurm-persist" mount_params: "powervault_iscsi" node_key: "local_hostname" node_mount_point: - "/var/lib/mysql" - "/var/spool/slurm" - slurm_conf_var: - - "StateSaveLocation" functional_group_prefix: ["slurm_control_node"] - name: powervault2 @@ -87,9 +86,25 @@ powervault_config: mount_point: "/mnt/slurmd-persist" mount_params: "powervault_iscsi" functional_group_prefix: ["slurm_node"] - #TODO: for powervault one more possibility is is to use one mount with source like powervault:powervault2 +beegfs_config: # this is not part of fstab mounts, it is handled by beegfs client configuration + - name: "beegfs_scratch" + mgmtd_host: "192.168.1.100" + mount_point: "/mnt/beegfs" + conn_auth_file: "/opt/omnia/beegfs1/connAuth" + client_conf_overrides: + tuneNumWorkers: 8 + functional_group_prefix: ["k8s_node"] + + - name: "beegfs_shared" + mgmtd_host: "192.168.2.100" + mount_point: "/mnt/beegfs-shared" + conn_auth_file: "/opt/omnia/beegfs2/connAuth" # TODO: This file on beegfs management node needs to be generated + client_conf_overrides: + tuneNumWorkers: 8 + # TODO: add more overrides if needed + functional_group_prefix: ["slurm_node"] # -----------------------------Cloud-Init Mounts------------------------------------------------ # mounts @@ -134,6 +149,7 @@ powervault_config: # Example: static mount with all explicit params (no profile) # mounts: + # - name: "atomic_mount" # source: "192.168.1.100:/export" # mount_point: "/mnt/data" @@ -152,33 +168,42 @@ powervault_config: # Example: per-node bind mount (node_key triggers bind behavior) # - name: "scratch_isolation" # source: "/mnt/scratch" +# mount_point: "/mnted/scratch" + # node_key: "local_hostname" # node_mount_point: # - "/scratch" # - "/tmp" -# slurm_conf_var: -# - "SlurmdSpoolDir" + # functional_group_prefix: ["slurm_node"] # # On node001 generates fstab: -# # /mnt/scratch/node001/scratch /scratch none bind 0 0 -# # /mnt/scratch/node001/tmp /tmp none bind 0 0 +# # /mnted/scratch/node001/scratch /scratch none bind 0 0 +# # /mnted/scratch/node001/tmp /tmp none bind 0 0 # # slurm.conf: SlurmdSpoolDir=/scratch,/tmp +# /mnt/scratch /mnted/sctratch nfs4 defaults,nofail,_netdev,x-systemd.after=cloud-init-network.service 0 0 + +# /mnted/scratch/node001/var/loig/state /var/loig/state none bind 0 0 + mounts: # Static mount: all explicit params, no profile, applies to all nodes - name: "atomic_mount" + # mount source: "UUID=" mount_point: "/mnt/atomic" + # fstab entries fs_type: "nfs" mnt_opts: "defaults,nofail,_netdev,x-systemd.after=cloud-init-network.service" dump_freq: "0" fsck_pass: "0" + functional_group_prefix: ["all"] # VAST NFS: shared export for home directories - name: "vast_home" source: "{{ vast_nfs_ip }}:/home" mount_point: "/home" mount_params: "vast_nfs" + # Where functional_group_prefix: ["slurm"] # VAST NFS: shared export for applications @@ -186,7 +211,7 @@ mounts: source: "{{ vast_nfs_ip }}:/apps" mount_point: "/apps" mount_params: "vast_nfs" - functional_group_prefix: ["slurm"] + functional_group_prefix: ["slurm", "login"] # VAST NFS: shared export for slurm state (controller) - name: "vast_slurm_state" @@ -195,14 +220,14 @@ mounts: mount_params: "vast_nfs" slurm_conf_var: - "StateSaveLocation" - functional_group_prefix: ["slurm_control_node"] + functional_group_prefix: ["slurm_control_node_x86_64"] # VAST NFS: shared scratch export - name: "vast_scratch_shared" source: "192.168.1.100:/scratch" mount_point: "/mnt/scratch" mount_params: "vast_nfs_performance" - functional_group_prefix: ["slurm_node"] + functional_group_prefix: ["slurm", "login"] # Per-node bind mount from shared scratch (node_key triggers bind behavior) - name: "scratch_isolation"