Skip to content
Open
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
15 changes: 15 additions & 0 deletions src/discovery/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Changelog

All notable changes to the `omnia.discovery` collection will be documented in this file.

## [3.0.0] - 2026-07-30

### Added
- Galaxy collection structure for discovery domain.
- Self-contained roles: `ome_discovery`, `discovery_setup`, `discovery_common`, `discovery_credentials`, `validate_discovery_input`.
- Plugins: `generate_discovery_report`, `generate_pxe_mapping`, `ome_server_inventory`, `validate_credentials`, `validate_discovery_config`.
- Input validation with JSON schemas for `discovery_config.yml` and `credential_rules.json`.
- L2 semantic validation flow for OME IP reachability checks.
- Domain-level documentation: `DISCOVERY_DESIGN.md`, `INPUT_CONTRACT.md`, `OUTPUT_CONTRACT.md`.
- All module references use FQCN (`ansible.builtin.*`).
- Zero `../common/` cross-domain imports.
4 changes: 2 additions & 2 deletions src/discovery/ansible.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[defaults]
log_path = /opt/omnia/log/core/discovery/discovery.log
remote_tmp = /opt/omnia/tmp/.ansible/tmp/
log_path = /var/log/omnia/discovery/discovery.log
remote_tmp = ~/.ansible/tmp/
host_key_checking = false
forks = 5
timeout = 180
Expand Down
82 changes: 82 additions & 0 deletions src/discovery/domain-init.sh

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

file name should be domain-init.sh

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/bin/bash

# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# =============================================================================
# domain-init.sh — Initialize discovery domain runtime directories and inputs
# =============================================================================
#
# Copies:
# 1. input/ config templates → <OMNIA_DATA_PATH>/discovery/input/<project>/
#
# Usage:
# ./domain-init.sh
# ./domain-init.sh --force
# OMNIA_DATA_PATH=/opt/omnia OMNIA_PROJECT_NAME=prod ./domain-init.sh
#
# Called automatically by: omnia.sh --setup-venv
# =============================================================================

set -euo pipefail

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly DOMAIN_NAME="discovery"

# --- Resolve environment variables ---
OMNIA_DATA_PATH="${OMNIA_DATA_PATH:-/opt/omnia}"
OMNIA_PROJECT_NAME="${OMNIA_PROJECT_NAME:-project_default}"
FORCE="${1:-}"

readonly INPUT_SRC="${SCRIPT_DIR}/input"
readonly INPUT_DST="${OMNIA_DATA_PATH}/input/${OMNIA_PROJECT_NAME}"
readonly LOG_DIR="/var/log/omnia/${DOMAIN_NAME}"

# --- Functions ---
log() { echo "[${DOMAIN_NAME}] $*"; }

ensure_dir() {
if [[ ! -d "$1" ]]; then
mkdir -p "$1"
log "Created directory: $1"
fi
}

copy_if_missing() {
local src="$1" dst="$2"
if [[ ! -f "$dst" ]] || [[ "$FORCE" == "--force" ]]; then
cp "$src" "$dst"
log "Copied: $(basename "$src") → $dst"
else
log "Skipped (exists): $(basename "$dst")"
fi
}

# --- Main ---
log "Setting up ${DOMAIN_NAME} domain..."

# Create required directories
ensure_dir "$INPUT_DST"
ensure_dir "$LOG_DIR"

# Copy input templates (only if not already present)
if [[ -d "$INPUT_SRC" ]]; then
for f in "$INPUT_SRC"/*; do
[[ -f "$f" ]] && copy_if_missing "$f" "${INPUT_DST}/$(basename "$f")"
done
else
log "No input/ directory found — skipping input copy"
fi

log "Domain setup complete."
2 changes: 1 addition & 1 deletion src/discovery/galaxy.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
namespace: omnia
name: discovery
version: 2.2.0
version: 3.0.0
readme: README.md
authors:
- Dell Technologies <omnia-support@dell.com>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
# Licensed under the Apache License, Version 2.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Discovery validation engine — runs L1 (schema) and L2 (logic) validation.

Provides the central ``run_validation()`` dispatcher that loads config files,
applies JSON schema checks, then runs semantic validators.
"""

import ipaddress
import json
import os

import yaml

from ..messages.discovery_messages import VALIDATOR_EXCEPTION_MSG


# ── Utility helpers ──────────────────────────────────────────────────────────

def is_valid_ipv4(addr):
"""Quick check for valid IPv4 address."""
try:
ip = ipaddress.ip_address(addr)
return ip.version == 4
except ValueError:
return False


def load_yaml_file(path):
"""Safely load a YAML file, return parsed data or None."""
try:
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
except (yaml.YAMLError, IOError, OSError):
return None


def load_json_schema(schema_path):
"""Load a JSON schema file, return parsed data or None."""
try:
with open(schema_path, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, IOError, OSError):
return None


def run_validation(config_file, config_data, validators, logger=None):
"""
Run a list of validator functions against config data.

Args:
config_file (str): Name of the config file being validated.
config_data (dict): Parsed configuration data.
validators (list): List of callables with signature (data, errors, logger).
logger: Optional logger instance.

Returns:
list: Collected error message strings (empty if valid).
"""
errors = []
for validator_fn in validators:
try:
validator_fn(config_data, errors, logger)
except Exception as e:
msg = VALIDATOR_EXCEPTION_MSG.format(config_file, validator_fn.__name__, e)
errors.append(msg)
if logger:
logger.error(msg)
return errors
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
# Licensed under the Apache License, Version 2.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Validation error/warning message constants for the discovery domain.

All message strings are defined here as UPPER_SNAKE_CASE constants to
keep validator logic free of inline strings.
"""

# ── discovery_config.yml messages ────────────────────────────────────────────

OME_IP_REQUIRED_MSG = "discovery_config: ome_ip is required when enable_bmc_discovery is true."
OME_IP_LOOPBACK_MSG = (
"discovery_config: ome_ip '{}' is a loopback address. "
"Provide the actual OME appliance IP."
)
OME_IP_INVALID_MSG = "discovery_config: ome_ip '{}' is not a valid IPv4 address."

# ── Engine messages ──────────────────────────────────────────────────────────

VALIDATOR_EXCEPTION_MSG = "{}: Validator {} raised: {}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
# Licensed under the Apache License, Version 2.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
L2 (semantic) validator for discovery_config.yml.

The ``validate()`` entry point runs all cross-field and semantic checks.
"""

import ipaddress

from ..messages.discovery_messages import (
OME_IP_REQUIRED_MSG,
OME_IP_LOOPBACK_MSG,
OME_IP_INVALID_MSG,
)


def _validate_ome_ip(config_data, errors, logger=None):
"""
Validate OME IP address logic.

Rules:
- If enable_bmc_discovery is true, ome_ip must be a valid, non-loopback IPv4.
- If enable_bmc_discovery is false, ome_ip is ignored.
"""
enable_bmc = config_data.get("enable_bmc_discovery", False)
if not enable_bmc:
return

ome_ip = config_data.get("ome_ip", "")
if not ome_ip:
errors.append(OME_IP_REQUIRED_MSG)
if logger:
logger.error(OME_IP_REQUIRED_MSG)
return

try:
addr = ipaddress.ip_address(ome_ip)
if addr.is_loopback:
msg = OME_IP_LOOPBACK_MSG.format(ome_ip)
errors.append(msg)
if logger:
logger.error(msg)
except ValueError:
msg = OME_IP_INVALID_MSG.format(ome_ip)
errors.append(msg)
if logger:
logger.error(msg)


def validate(config_data, errors, logger=None):
"""
Run all L2 validators for discovery_config.yml.

Args:
config_data (dict): Parsed discovery_config.yml content.
errors (list): Mutable list to append error messages to.
logger: Optional logger instance.
"""
_validate_ome_ip(config_data, errors, logger)
17 changes: 17 additions & 0 deletions src/discovery/plugins/modules/validate_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@
type: str
'''

EXAMPLES = r'''
- name: Validate a credential field
omnia.discovery.validate_credentials:
credential_field: ome_password
credential_input: "{{ ome_password }}"
module_utils_path: "{{ role_path }}/../../plugins/module_utils"
register: validation_result
no_log: true
'''

RETURN = r'''
msg:
description: Validation result message.
type: str
returned: always
'''


def load_rules(file_path):
"""Loads validation rules from a JSON file."""
Expand Down
19 changes: 19 additions & 0 deletions src/discovery/plugins/modules/validate_discovery_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@
type: str
'''

EXAMPLES = r'''
- name: Validate discovery configuration files
omnia.discovery.validate_discovery_config:
input_project_dir: /opt/omnia/input/project_default
schema_dir: "{{ role_path }}/../../plugins/module_utils/discovery_validation/schema"
register: validation_result
'''

RETURN = r'''
msg:
description: Validation summary message.
type: str
returned: always
validation_errors:
description: List of validation errors found, if any.
type: list
returned: failure
'''

VALIDATION_LOG_PATH = "/opt/omnia/log/core/playbooks/"

# Files to validate and their corresponding schema names
Expand Down
4 changes: 4 additions & 0 deletions src/discovery/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Python dependencies for omnia.discovery collection
ansible-core>=2.20
jinja2>=3.0
pyyaml>=5.4
6 changes: 6 additions & 0 deletions src/discovery/requirements.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
collections:
- name: community.general
version: ">=5.0.0"
- name: ansible.posix
version: ">=1.4.0"
9 changes: 9 additions & 0 deletions src/discovery/roles/ome_discovery/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,12 @@

- name: Generate BMC discovery report
ansible.builtin.include_tasks: generate_discovery_report.yml

- name: Write discovery status file
ansible.builtin.copy:
content: |
discovery_status: complete
discovery_mechanism: "{{ discovery_mechanism | default('ome') }}"
timestamp: "{{ ansible_date_time.iso8601 | default('unknown') }}"
dest: "{{ discovery_output_dir | default('/opt/omnia/output/project_default/discovery') }}/discovery_status.yml"
mode: '0644'
Loading
Loading