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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 91 additions & 30 deletions src/discovery/plugins/modules/generate_pxe_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import csv
import os
import re
from typing import Optional
from ansible.module_utils.basic import AnsibleModule

DOCUMENTATION = r'''
Expand Down Expand Up @@ -71,6 +72,16 @@
required: false
type: str
default: ""
os_type:
description: OS type to embed in functional group names (e.g. rhel, rocky, ubuntu)
required: false
type: str
default: ""
os_version:
description: OS version to embed in functional group names (e.g. 10.0, 10.2). Dots are replaced with underscores.
required: false
type: str
default: ""
author:
- Dell Inc.
'''
Expand Down Expand Up @@ -101,31 +112,71 @@


DEFAULT_FUNCTIONAL_GROUP = "slurm_node_aarch64"
PARENT_TAG_SOURCE_GROUP = "service_kube_node_x86_64"

# Omnia-supported functional group names.
# Only servers whose OME static group matches one of these will be
# included in the PXE mapping file.
SUPPORTED_FUNCTIONAL_GROUPS = {
"service_kube_control_plane_x86_64",
"service_kube_node_x86_64",
"login_node_x86_64",
"login_node_aarch64",
"login_compiler_node_x86_64",
"login_compiler_node_aarch64",
"slurm_control_node_x86_64",
"slurm_node_x86_64",
"slurm_node_aarch64",
"os_x86_64",
"os_aarch64",
}

# Roles that receive PARENT_SERVICE_TAG (set to a service_kube_node_x86_64
PARENT_TAG_SOURCE_PREFIX = "service_kube_node_"

# Supported functional group role prefixes.
# OME static group names must match one of these prefixes (followed by an
# optional OS version segment and an architecture suffix) to be included
# in the PXE mapping file.
SUPPORTED_ROLE_PREFIXES = (
"service_kube_control_plane_",
"service_kube_node_",
"login_node_",
"login_compiler_node_",
"slurm_control_node_",
"slurm_node_",
"os_",
)

SUPPORTED_ARCHITECTURES = ("x86_64", "aarch64")

# Pattern to detect an optional OS-version segment before the architecture
# suffix (e.g. _rhel_10_0 in slurm_node_rhel_10_0_x86_64).
_ARCH_PATTERN = re.compile(r"_(?P<arch>x86_64|aarch64)$")

# Roles that receive PARENT_SERVICE_TAG (set to a service_kube_node
# service tag from the same Scalable Unit).
CHILD_ROLES_WITH_PARENT_TAG = {
"slurm_node_aarch64",
"slurm_node_x86_64",
}
CHILD_ROLE_PREFIXES = ("slurm_node_",)


def _is_supported_functional_group(group_name: str) -> bool:
"""Check if an OME group name matches a supported Omnia role pattern."""
if not group_name:
return False
lower = group_name.lower()
if not lower.startswith(SUPPORTED_ROLE_PREFIXES):
return False
return bool(_ARCH_PATTERN.search(group_name))


def _inject_os_version(
fg_name: str,
os_type: Optional[str],
os_version: Optional[str],
) -> str:
"""Insert _{os_type}_{version} before the architecture suffix.

If the name already contains an OS version segment, leave it unchanged.
If os_type or os_version are empty, return the name unchanged.
Dots in os_version are replaced with underscores (10.0 -> 10_0).
"""
if not os_type or not os_version:
return fg_name
arch_match = _ARCH_PATTERN.search(fg_name)
if arch_match is None:
return fg_name

# Check if an OS version segment is already present
prefix = fg_name[:arch_match.start()]
os_segment_pattern = re.compile(
r"_(?:rhel|rocky|ubuntu|sles)(?:_[0-9]+)+$"
)
if os_segment_pattern.search(prefix):
return fg_name

normalized_version = os_version.replace(".", "_")
arch = arch_match.group("arch")
return f"{prefix}_{os_type}_{normalized_version}_{arch}"


def extract_su_from_hostname(bmc_hostname):
Expand Down Expand Up @@ -196,7 +247,9 @@ def main():
"hostname_start": {"type": "int", "required": False, "default": 1},
"hostname_padding": {"type": "int", "required": False, "default": 3},
"ib_subnet": {"type": "str", "required": False, "default": ""},
"admin_subnet": {"type": "str", "required": False, "default": ""}
"admin_subnet": {"type": "str", "required": False, "default": ""},
"os_type": {"type": "str", "required": False, "default": ""},
"os_version": {"type": "str", "required": False, "default": ""},
}

module = AnsibleModule(
Expand All @@ -213,6 +266,8 @@ def main():
hostname_padding = module.params['hostname_padding']
ib_subnet = module.params['ib_subnet']
admin_subnet = module.params['admin_subnet']
os_type = module.params['os_type'].strip().lower()
os_version = module.params['os_version'].strip()

# CSV headers as specified
headers = [
Expand Down Expand Up @@ -253,16 +308,19 @@ def main():
server_group = server.get('group_name', '').strip()

# Skip servers whose OME group is not a supported Omnia functional group
if server_group and server_group not in SUPPORTED_FUNCTIONAL_GROUPS:
if server_group and not _is_supported_functional_group(server_group):
svc_tag = server.get('service_tag', 'unknown')
module.warn(
f"Skipping device {svc_tag}: OME static group '{server_group}' "
f"is not a supported Omnia functional group. "
f"Supported groups: {', '.join(sorted(SUPPORTED_FUNCTIONAL_GROUPS))}"
f"Supported prefixes: {', '.join(SUPPORTED_ROLE_PREFIXES)}"
)
continue

resolved_functional_group = server_group if server_group else functional_group
resolved_functional_group = _inject_os_version(
resolved_functional_group, os_type, os_version
)

# Derive GROUP_NAME: try SU from BMC hostname first,
# then from OME group name, then fall back to module default (grp0)
Expand All @@ -289,15 +347,18 @@ def main():
# Build SU -> service_kube_node service tag map
su_kube_node_map = {}
for row in rows:
if row["FUNCTIONAL_GROUP_NAME"] == PARENT_TAG_SOURCE_GROUP:
if row["FUNCTIONAL_GROUP_NAME"].startswith(PARENT_TAG_SOURCE_PREFIX):
su = row["GROUP_NAME"]
if su and su not in su_kube_node_map:
su_kube_node_map[su] = row["SERVICE_TAG"]

# Assign PARENT_SERVICE_TAG only to slurm_node roles,
# using a service_kube_node_x86_64 service tag from the same GROUP_NAME
# using a service_kube_node service tag from the same GROUP_NAME
for row in rows:
if row["FUNCTIONAL_GROUP_NAME"] not in CHILD_ROLES_WITH_PARENT_TAG:
if not any(
row["FUNCTIONAL_GROUP_NAME"].startswith(p)
for p in CHILD_ROLE_PREFIXES
):
continue
su = row["GROUP_NAME"]
if su in su_kube_node_map:
Expand Down
7 changes: 7 additions & 0 deletions src/discovery/roles/ome_discovery/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ hostname_start_number: 1
# Hostname padding (e.g., 3 digits = nid001, must be 3 for orchestrator NID support: nid000-nid999)
hostname_padding: 3

# OS type and version to embed in functional group names.
# When set, discovery appends _{os_type}_{os_version} before the architecture
# suffix (e.g. slurm_node_x86_64 -> slurm_node_rhel_10_0_x86_64).
# Sourced from the active catalog or set explicitly.
discovery_os_type: "{{ cluster_os_type | default('') }}"
discovery_os_version: "{{ cluster_os_version | default('') }}"

# OME device type for servers
ome_server_device_type: 1000

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,29 @@
file: "{{ discovery_input_dir }}/network_spec.yml"
name: network_spec_data

- name: Parse catalog to extract OS type and version
when: catalog_file is defined
vars:
catalog_file: "{{ lookup('env', 'CATALOG_FILE_PATH') | default('/omnia/catalog/catalog_rhel.json') }}"
block:
- name: Check if catalog file exists
ansible.builtin.stat:
path: "{{ catalog_file }}"
register: catalog_stat

- name: Parse catalog JSON to extract OS metadata
ansible.builtin.set_fact:
_catalog_data: "{{ lookup('file', catalog_file) | from_yaml }}"
when: catalog_stat.stat.exists

- name: Extract OS type and version from catalog
ansible.builtin.set_fact:
discovery_os_type: "{{ _catalog_data.catalog.groups.baseos_group.os | default('') }}"
discovery_os_version: "{{ _catalog_data.catalog.groups.baseos_group.os_version | default('') }}"
when:
- catalog_stat.stat.exists
- _catalog_data.catalog.groups.baseos_group is defined

- name: Set admin_subnet from network spec
ansible.builtin.set_fact:
admin_subnet: "{{ (network_spec_data.Networks | selectattr('admin_network', 'defined') | first).admin_network.subnet | default('') }}"
Expand Down Expand Up @@ -55,6 +78,8 @@
hostname_padding: "{{ hostname_padding }}"
ib_subnet: "{{ ib_subnet }}"
admin_subnet: "{{ admin_subnet }}"
os_type: "{{ discovery_os_type | default('') }}"
os_version: "{{ discovery_os_version | default('') }}"
register: pxe_mapping_result

- name: Create symlink to latest PXE mapping file
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,15 @@ def pxe_mapping_architecture_mismatch_msg(
)


def pxe_mapping_missing_os_version_msg(value: str, row: int) -> str:
"""Return a missing OS version suffix error message."""
return (
f"orchestrator_config: FUNCTIONAL_GROUP_NAME '{value}' at mapping "
f"row {row} must include an OS version suffix "
"(e.g. _rhel_10_0) before the architecture suffix."
)


def pxe_mapping_unknown_catalog_group_msg(
value: str, row: int, path: str
) -> str:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,18 @@ def _validate_names(
functional_group, row_number
),
)
elif functional_group and functional_group.lower().startswith(
CATALOG_MANAGED_PREFIXES
):
identity = _functional_group_identity(functional_group)
if identity is not None and identity[1] is None:
record_error(
errors,
logger,
msg.pxe_mapping_missing_os_version_msg(
functional_group, row_number
),
)


def _validate_ib_pair(
Expand Down Expand Up @@ -493,10 +505,7 @@ def _catalog_matches(
or mapping_architecture != catalog_architecture
):
continue
if (
mapping_os_version is None
or mapping_os_version == catalog_os_version
):
if mapping_os_version == catalog_os_version:
return True
return False

Expand Down
Loading