diff --git a/.metadata/omnia_version b/.metadata/omnia_version
index 33deddc067..97770c42fc 100644
--- a/.metadata/omnia_version
+++ b/.metadata/omnia_version
@@ -1,2 +1,2 @@
-omnia_version: 2.0.0.0
+omnia_version: 2.2.0.1
omnia_installation_path: ""
diff --git a/README.md b/README.md
index 5c93e6f600..de52fdd1d9 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,14 @@
+
+----
+
+**Please note**: We take Omnia's security and our users' trust
+very seriously. If you believe you have found a security issue
+in Omnia, _please responsibly disclose_ by following the process at
+[https://github.com/dell/omnia/security/advisories](https://github.com/dell/omnia/blob/main/SECURITY.md).
+
+----
+
diff --git a/SECURITY.md b/SECURITY.md
index 2c3acae508..39d1d65606 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -10,7 +10,7 @@ Only the latest released version of Omnia is supported with security updates. Us
If you discover a security vulnerability in Omnia, please do **not** create a public GitHub issue.
-Please report it using GitHub's **Private Vulnerability Reporting** feature.
+Please report it using GitHub's [**Private Vulnerability Reporting**](https://github.com/dell/omnia/security/advisories) feature.
Please include:
@@ -35,6 +35,6 @@ Please avoid public disclosure until the issue has been reviewed and a fix is av
## Contact
-For security-related concerns, please use GitHub's **Private Vulnerability Reporting** feature.
+For security-related concerns, please use GitHub's [**Private Vulnerability Reporting**](https://github.com/dell/omnia/security/advisories) feature.
Thank you for helping make Omnia more secure.
diff --git a/common/library/module_utils/upgrade/upgrade_hop_calculator_lib.py b/common/library/module_utils/upgrade/upgrade_hop_calculator_lib.py
index d888ebdd56..82f75fcc77 100644
--- a/common/library/module_utils/upgrade/upgrade_hop_calculator_lib.py
+++ b/common/library/module_utils/upgrade/upgrade_hop_calculator_lib.py
@@ -33,7 +33,7 @@
the system auto-generates one K8s hop per intermediate version.
Example: Omnia 2.1.0.0 (K8s 1.34.1) -> Omnia 2.3.0.0 (K8s 1.37.1)
- Omnia path : 2.1.0.0 -> 2.2.0.0 -> 2.3.0.0
+ Omnia path : 2.1.0.0 -> 2.2.0.1 -> 2.3.0.0
K8s hops : 1.34.1 -> 1.35.1 (Omnia 2.1->2.2, direct)
1.35.1 -> 1.36.1 (auto-generated, within 2.2->2.3)
1.36.1 -> 1.37.1 (Omnia 2.2->2.3, final)
diff --git a/common/library/modules/delete_idracips_from_mysqldb.py b/common/library/modules/delete_idracips_from_mysqldb.py
index cd81b943e2..c022d5f1dd 100644
--- a/common/library/modules/delete_idracips_from_mysqldb.py
+++ b/common/library/modules/delete_idracips_from_mysqldb.py
@@ -14,13 +14,15 @@
#!/usr/bin/python
"""Module to delete iDRAC IPs from MySQL database.
-This module connects to a Kubernetes pod running MySQL and deletes iDRAC IPs
-that are not present in bmc_data.csv. It handles retries and delays for robustness."""
+This module connects to a Kubernetes pod running MySQL via PyMySQL and deletes
+iDRAC IPs that are not present in bmc_data.csv. It uses parameterized queries
+to prevent SQL injection. It handles retries and delays for robustness."""
import time
+import ipaddress
+import pymysql
from ansible.module_utils.basic import AnsibleModule
from kubernetes import client, config
-from kubernetes.stream import stream
from kubernetes.config.config_exception import ConfigException
@@ -32,83 +34,82 @@ def load_kube_context():
config.load_incluster_config()
-def run_mysql_query_in_pod(namespace, pod, container, mysql_user, mysql_password, query):
- """Run a MySQL query in the specified pod.
+def resolve_pod_ip(namespace, pod):
+ """Resolve the IP address of a Kubernetes pod via the K8s API.
Args:
namespace: Kubernetes namespace
pod: Pod name
- container: Container name
- mysql_user: MySQL username
- mysql_password: MySQL password
- query: MySQL query to execute
Returns:
- dict: Result containing return code and output
+ str: Pod IP address
+
+ Raises:
+ RuntimeError: If the pod IP cannot be resolved
"""
core_v1 = client.CoreV1Api()
- mysql_command = [
- "mysql",
- "-u", mysql_user,
- "-N", "-B",
- f"-p{mysql_password}",
- "-e", query
- ]
-
- try:
- ws = stream(
- core_v1.connect_get_namespaced_pod_exec,
- name=pod,
- namespace=namespace,
- container=container,
- command=mysql_command,
- stderr=True,
- stdin=False,
- stdout=True,
- tty=False,
- _preload_content=False
- )
+ pod_obj = core_v1.read_namespaced_pod(name=pod, namespace=namespace)
+ pod_ip = pod_obj.status.pod_ip
+ if not pod_ip:
+ raise RuntimeError(f"Pod {pod} in namespace {namespace} has no IP assigned")
+ return pod_ip
- stdout = ""
- stderr = ""
- while ws.is_open():
- ws.update(timeout=1)
- if ws.peek_stdout():
- stdout += ws.read_stdout()
- if ws.peek_stderr():
- stderr += ws.read_stderr()
- ws.close()
+def run_mysql_delete_in_pod(
+ namespace, pod, mysqldb_container_port, mysqldb_name,
+ mysql_user, mysql_password, ip_to_delete
+):
+ """Delete an iDRAC IP from MySQL using a PyMySQL parameterized query.
- rc = ws.returncode
+ Connects directly to the MySQL pod over TCP (resolved via the K8s API)
+ and executes a DELETE with a bound parameter, eliminating SQL injection.
- if rc != 0:
- return {
- "rc": rc,
- "result": stderr.strip() if stderr else "Unknown error"
- }
+ Args:
+ namespace: Kubernetes namespace
+ pod: Pod name
+ mysqldb_container_port: MySQL container port (default 3306)
+ mysqldb_name: MySQL database name
+ mysql_user: MySQL username
+ mysql_password: MySQL password
+ ip_to_delete: IP address to delete
- query_result = [
- line.strip() for line in stdout.strip().splitlines()
- if line.strip() and not line.strip().startswith("mysql:")
- ]
+ Returns:
+ dict: Result containing return code and output
+ """
+ pod_ip = resolve_pod_ip(namespace, pod)
+ conn = None
+ try:
+ conn = pymysql.connect(
+ host=pod_ip,
+ port=mysqldb_container_port,
+ user=mysql_user,
+ password=mysql_password,
+ database=mysqldb_name,
+ connect_timeout=10
+ )
+ with conn.cursor() as cursor:
+ cursor.execute("DELETE FROM services WHERE ip = %s", (ip_to_delete,))
+ affected_rows = cursor.rowcount
+ conn.commit()
return {
- "rc": rc,
- "result": query_result
+ "rc": 0,
+ "result": f"Deleted {affected_rows} row(s)"
}
-
- except (ConfigException, OSError) as e:
+ except (pymysql.err.OperationalError, pymysql.err.MySQLError) as e:
return {
"rc": 1,
"result": str(e)
}
+ finally:
+ if conn:
+ conn.close()
def delete_idrac_from_mysql(
namespace,
pod,
- container,
+ mysqldb_container_port,
mysqldb_name,
mysql_user,
mysql_password,
@@ -121,7 +122,7 @@ def delete_idrac_from_mysql(
Args:
namespace: Kubernetes namespace
pod: Pod name
- container: Container name
+ mysqldb_container_port: MySQL container port
mysqldb_name: MySQL database name
mysql_user: MySQL username
mysql_password: MySQL password
@@ -132,19 +133,26 @@ def delete_idrac_from_mysql(
Returns:
dict: Result containing success status and message
"""
- query = (
- f"DELETE FROM {mysqldb_name}.services "
- f"WHERE ip = '{ip_to_delete}';"
- )
+ # Defense-in-depth: validate IP before attempting DB operation
+ try:
+ ipaddress.ip_address(ip_to_delete)
+ except ValueError:
+ return {
+ "success": False,
+ "ip": ip_to_delete,
+ "msg": f"Invalid IP address format: {ip_to_delete}"
+ }
+ result = {}
for attempt in range(retries):
- result = run_mysql_query_in_pod(
+ result = run_mysql_delete_in_pod(
namespace=namespace,
pod=pod,
- container=container,
+ mysqldb_container_port=mysqldb_container_port,
+ mysqldb_name=mysqldb_name,
mysql_user=mysql_user,
mysql_password=mysql_password,
- query=query
+ ip_to_delete=ip_to_delete
)
if result.get("rc") == 0:
@@ -177,13 +185,13 @@ def main():
"pod_to_db_idrac_ips": {"type": "dict", "required": True},
"db_retries": {"type": "int", "default": 3},
"db_delay": {"type": "int", "default": 3},
+ "mysqldb_container_port": {"type": "int", "default": 3306},
}
module = AnsibleModule(argument_spec=module_args, supports_check_mode=True)
telemetry_namespace = module.params["telemetry_namespace"]
idrac_podnames = module.params["idrac_podnames"]
- mysqldb_k8s_name = module.params["mysqldb_k8s_name"]
mysqldb_name = module.params["mysqldb_name"]
mysqldb_user = module.params["mysqldb_user"]
mysqldb_password = module.params["mysqldb_password"]
@@ -191,6 +199,7 @@ def main():
pod_to_db_idrac_ips = module.params["pod_to_db_idrac_ips"]
db_retries = module.params["db_retries"]
db_delay = module.params["db_delay"]
+ mysqldb_container_port = module.params["mysqldb_container_port"]
load_kube_context()
@@ -213,7 +222,7 @@ def main():
result = delete_idrac_from_mysql(
namespace=telemetry_namespace,
pod=pod,
- container=mysqldb_k8s_name,
+ mysqldb_container_port=mysqldb_container_port,
mysqldb_name=mysqldb_name,
mysql_user=mysqldb_user,
mysql_password=mysqldb_password,
diff --git a/common/library/modules/insert_idracips_mysqldb.py b/common/library/modules/insert_idracips_mysqldb.py
index 74b6bd8858..dc1ded5f32 100644
--- a/common/library/modules/insert_idracips_mysqldb.py
+++ b/common/library/modules/insert_idracips_mysqldb.py
@@ -14,15 +14,17 @@
#!/usr/bin/python
"""Module to insert iDRAC IPs into MySQL database.
-This module connects to a Kubernetes pod running MySQL and inserts iDRAC IPs with
-associated service type and authentication details.
+This module connects to a Kubernetes pod running MySQL via PyMySQL and inserts
+iDRAC IPs with associated service type and authentication details.
+It uses parameterized queries to prevent SQL injection.
It handles retries and delays for robustness."""
import time
import json
+import ipaddress
+import pymysql
from ansible.module_utils.basic import AnsibleModule
from kubernetes import client, config
-from kubernetes.stream import stream
from kubernetes.config.config_exception import ConfigException
def load_kube_context():
@@ -32,14 +34,30 @@ def load_kube_context():
except ConfigException:
config.load_incluster_config()
-def escape_single_quotes(s):
- """Escape single quotes in a string for safe MySQL insertion."""
- return s.replace("'", "\\'")
+def resolve_pod_ip(namespace, pod):
+ """Resolve the IP address of a Kubernetes pod via the K8s API.
+
+ Args:
+ namespace: Kubernetes namespace
+ pod: Pod name
+
+ Returns:
+ str: Pod IP address
+
+ Raises:
+ RuntimeError: If the pod IP cannot be resolved
+ """
+ core_v1 = client.CoreV1Api()
+ pod_obj = core_v1.read_namespaced_pod(name=pod, namespace=namespace)
+ pod_ip = pod_obj.status.pod_ip
+ if not pod_ip:
+ raise RuntimeError(f"Pod {pod} in namespace {namespace} has no IP assigned")
+ return pod_ip
def run_mysql_insert(
namespace,
pod,
- container,
+ mysqldb_container_port,
mysqldb_name,
mysql_user,
mysql_password,
@@ -48,73 +66,63 @@ def run_mysql_insert(
auth_type,
auth_json
):
- """Run a MySQL insert command in the specified pod."""
+ """Run a MySQL insert using a PyMySQL parameterized query.
- query = (
- f"INSERT IGNORE INTO {mysqldb_name}.services "
- f"(ip, serviceType, authType, auth) VALUES ("
- f"'{ip}', "
- f"'{service_type}', "
- f"'{auth_type}', "
- f"'{escape_single_quotes(auth_json)}'"
- f");"
- )
+ Connects directly to the MySQL pod over TCP (resolved via the K8s API)
+ and executes an INSERT IGNORE with bound parameters, eliminating SQL injection.
- command = [
- "mysql", "-u", mysql_user, f"-p{mysql_password}",
- "-e", query
- ]
+ Args:
+ namespace: Kubernetes namespace
+ pod: Pod name
+ mysqldb_container_port: MySQL container port
+ mysqldb_name: MySQL database name
+ mysql_user: MySQL username
+ mysql_password: MySQL password
+ ip: iDRAC IP address to insert
+ service_type: Service type value
+ auth_type: Authentication type value
+ auth_json: JSON string of authentication credentials
- core_v1 = client.CoreV1Api()
+ Returns:
+ dict: Result containing rc (bool) and result message
+ """
+ pod_ip = resolve_pod_ip(namespace, pod)
+
+ conn = None
try:
- ws = stream(
- core_v1.connect_get_namespaced_pod_exec,
- name=pod,
- namespace=namespace,
- container=container,
- command=command,
- stderr=True,
- stdin=False,
- stdout=True,
- tty=False,
- _preload_content=False # Allows streaming access
+ conn = pymysql.connect(
+ host=pod_ip,
+ port=mysqldb_container_port,
+ user=mysql_user,
+ password=mysql_password,
+ database=mysqldb_name,
+ connect_timeout=10
)
-
- stdout = ""
- stderr = ""
-
- while ws.is_open():
- ws.update(timeout=1)
- if ws.peek_stdout():
- stdout += ws.read_stdout()
- if ws.peek_stderr():
- stderr += ws.read_stderr()
- ws.close()
-
- rc = ws.returncode
-
- if rc != 0:
- return {
- "rc": False,
- "result": stderr.strip() or "Unknown error"
- }
+ with conn.cursor() as cursor:
+ cursor.execute(
+ "INSERT IGNORE INTO services (ip, serviceType, authType, auth) "
+ "VALUES (%s, %s, %s, %s)",
+ (ip, service_type, auth_type, auth_json)
+ )
+ conn.commit()
return {
"rc": True,
- "result": stdout.strip()
+ "result": f"Inserted IP {ip}"
}
-
- except Exception as e:
- # Catching all to ensure MySQL errors or stream failures are handled
+ except (pymysql.err.OperationalError, pymysql.err.MySQLError) as e:
return {
- "rc": False,
+ "rc": False,
"result": str(e)
}
+ finally:
+ if conn:
+ conn.close()
def insert_idracs_to_mysql(
namespace,
pod,
- container,
+ mysqldb_container_port,
mysqldb_name,
mysql_user,
mysql_password,
@@ -136,11 +144,19 @@ def insert_idracs_to_mysql(
try:
for ip in telemetry_idrac_list:
+ # Defense-in-depth: validate IP before attempting DB operation
+ try:
+ ipaddress.ip_address(ip)
+ except ValueError:
+ results.append({"ip": ip, "changed": False,
+ "msg": f"Invalid IP address format: {ip}"})
+ continue
+
for _ in range(retries):
result = run_mysql_insert(
namespace=namespace,
pod=pod,
- container=container,
+ mysqldb_container_port=mysqldb_container_port,
mysqldb_name=mysqldb_name,
mysql_user=mysql_user,
mysql_password=mysql_password,
@@ -156,7 +172,7 @@ def insert_idracs_to_mysql(
time.sleep(delay)
else:
results.append({"ip": ip, "changed": False, \
- "msg": f"Failed after {retries} attempts: {msg}"})
+ "msg": f"Failed after {retries} attempts: {result.get('result')}"})
if not results:
results.append({"ip": "unknown", "changed": False, \
"msg": "No iDRAC IPs to insert."})
@@ -182,6 +198,7 @@ def main():
"auth_type": {"type": "str", "required": True},
"db_retries": {"type": "int", "required": False, "default": 3},
"db_delay": {"type": "int", "required": False, "default": 3},
+ "mysqldb_container_port": {"type": "int", "default": 3306},
}
result = {
@@ -197,7 +214,6 @@ def main():
telemetry_namespace = module.params['telemetry_namespace']
idrac_podnames_ips = module.params['idrac_podnames_ips']
- mysqldb_k8s_name = module.params['mysqldb_k8s_name']
mysqldb_name = module.params['mysqldb_name']
mysql_user = module.params['mysql_user']
mysqldb_password = module.params['mysqldb_password']
@@ -208,6 +224,7 @@ def main():
auth_type = module.params['auth_type']
db_retries = module.params['db_retries']
db_delay = module.params['db_delay']
+ mysqldb_container_port = module.params['mysqldb_container_port']
# For each pod in idrac_podnames,
# fetch the working IP's from telemetry_idrac,
@@ -222,7 +239,7 @@ def main():
pod_results = insert_idracs_to_mysql(
namespace=telemetry_namespace,
pod=pod,
- container=mysqldb_k8s_name,
+ mysqldb_container_port=mysqldb_container_port,
mysqldb_name=mysqldb_name,
mysql_user=mysql_user,
mysql_password=mysqldb_password,
diff --git a/common/library/modules/read_idracips_from_mysqldb.py b/common/library/modules/read_idracips_from_mysqldb.py
index b73877bf35..cbe0a48314 100644
--- a/common/library/modules/read_idracips_from_mysqldb.py
+++ b/common/library/modules/read_idracips_from_mysqldb.py
@@ -14,12 +14,14 @@
#!/usr/bin/python
"""Module to read iDRAC IPs from MySQL database.
-This module connects to a Kubernetes pod running MySQL and retrieves iDRAC IPs
-from the 'services' table. It handles retries and delays for robustness."""
+This module connects to a Kubernetes pod running MySQL via PyMySQL and retrieves
+iDRAC IPs from the 'services' table. It uses parameterized queries (database
+selected via connection kwarg) to prevent SQL injection.
+It handles retries and delays for robustness."""
import time
+import pymysql
from ansible.module_utils.basic import AnsibleModule
from kubernetes import client, config
-from kubernetes.stream import stream
def load_kube_context():
"""Load Kubernetes configuration for accessing the cluster."""
@@ -29,67 +31,91 @@ def load_kube_context():
config.load_incluster_config()
-# Function to execute a MySQL command inside a pod using the Kubernetes client
-def run_mysql_query_in_pod(namespace, pod, container, mysql_user, mysql_password, query):
- """Run a MySQL query in the specified pod."""
- core_v1 = client.CoreV1Api()
- mysql_command = [
- "mysql",
- "-u", mysql_user,
- "-N", "-B",
- f"-p{mysql_password}",
- "-e", query
- ]
-
- try:
- ws = stream(
- core_v1.connect_get_namespaced_pod_exec,
- name=pod,
- namespace=namespace,
- container=container,
- command=mysql_command,
- stderr=True,
- stdin=False,
- stdout=True,
- tty=False,
- _preload_content=False # Allow access to return code and streaming output
- )
+def resolve_pod_ip(namespace, pod):
+ """Resolve the IP address of a Kubernetes pod via the K8s API.
- stdout = ""
- stderr = ""
+ Args:
+ namespace: Kubernetes namespace
+ pod: Pod name
- while ws.is_open():
- ws.update(timeout=1)
- if ws.peek_stdout():
- stdout += ws.read_stdout()
- if ws.peek_stderr():
- stderr += ws.read_stderr()
- ws.close()
+ Returns:
+ str: Pod IP address
- rc = ws.returncode
-
- if rc != 0:
- return {
- "rc": rc,
- "result": stderr.strip() if stderr else "Unknown error"
- } # Or return stderr if you want to inspect/log errors
-
- # Clean and filter result
- query_result = [
- line.strip() for line in stdout.strip().splitlines()
- if line.strip() and not line.strip().startswith("mysql:")
- ]
+ Raises:
+ RuntimeError: If the pod IP cannot be resolved
+ """
+ core_v1 = client.CoreV1Api()
+ pod_obj = core_v1.read_namespaced_pod(name=pod, namespace=namespace)
+ pod_ip = pod_obj.status.pod_ip
+ if not pod_ip:
+ raise RuntimeError(f"Pod {pod} in namespace {namespace} has no IP assigned")
+ return pod_ip
+
+
+# Function to check for the services table and read IPs via PyMySQL
+def run_mysql_read_in_pod(
+ namespace, pod, mysqldb_container_port, mysqldb_name,
+ mysql_user, mysql_password
+):
+ """Read iDRAC IPs from MySQL using a PyMySQL connection.
+
+ Connects directly to the MySQL pod over TCP (resolved via the K8s API)
+ with the database selected via connection kwarg (no identifier interpolation).
+
+ Args:
+ namespace: Kubernetes namespace
+ pod: Pod name
+ mysqldb_container_port: MySQL container port
+ mysqldb_name: MySQL database name
+ mysql_user: MySQL username
+ mysql_password: MySQL password
+
+ Returns:
+ dict: Result with 'tables_found' (bool or result), 'ip_list' (list), 'rc' (int)
+ """
+ pod_ip = resolve_pod_ip(namespace, pod)
+
+ conn = None
+ try:
+ conn = pymysql.connect(
+ host=pod_ip,
+ port=mysqldb_container_port,
+ user=mysql_user,
+ password=mysql_password,
+ database=mysqldb_name,
+ connect_timeout=10
+ )
+ with conn.cursor() as cursor:
+ # Check for services table (schema already selected via connection)
+ cursor.execute("SHOW TABLES")
+ tables = [row[0] for row in cursor.fetchall()]
+ if "services" not in tables:
+ return {
+ "rc": 0,
+ "tables_found": None,
+ "ip_list": []
+ }
+
+ # Fetch iDRAC IPs
+ cursor.execute("SELECT ip FROM services")
+ ip_list = [row[0] for row in cursor.fetchall()]
return {
- "rc": rc,
- "result": query_result
+ "rc": 0,
+ "tables_found": tables,
+ "ip_list": ip_list
}
-
- except Exception as e:
+ except (pymysql.err.OperationalError, pymysql.err.MySQLError) as e:
return {
- "rc": 1,
- "result": str(e)
+ "rc": 1,
+ "tables_found": None,
+ "ip_list": [],
+ "result": str(e)
}
+ finally:
+ if conn:
+ conn.close()
+
def main():
"""Main function to execute the module logic."""
@@ -102,18 +128,19 @@ def main():
"mysqldb_password": {"type": "str", "required": True, "no_log": True},
"db_retries": {"type": "int", "default": 5},
"db_delay": {"type": "int", "default": 3},
+ "mysqldb_container_port": {"type": "int", "default": 3306},
}
module = AnsibleModule(argument_spec=module_args, supports_check_mode=True)
telemetry_namespace = module.params["telemetry_namespace"]
idrac_podnames = module.params["idrac_podnames"]
- mysqldb_k8s_name = module.params["mysqldb_k8s_name"]
mysqldb_name = module.params["mysqldb_name"]
mysqldb_user = module.params["mysqldb_user"]
mysqldb_password = module.params["mysqldb_password"]
db_retries = module.params["db_retries"]
db_delay = module.params["db_delay"]
+ mysqldb_container_port = module.params["mysqldb_container_port"]
load_kube_context()
@@ -124,37 +151,21 @@ def main():
try:
for idrac_podname in idrac_podnames:
found = None
- ip_output = None
ip_list = []
for _ in range(db_retries):
- # Check for services table
- query_tables = f"SHOW TABLES FROM {mysqldb_name}"
- tables_output = run_mysql_query_in_pod(
- telemetry_namespace,
- idrac_podname,
- mysqldb_k8s_name,
- mysqldb_user,
- mysqldb_password,
- query_tables
+ read_result = run_mysql_read_in_pod(
+ namespace=telemetry_namespace,
+ pod=idrac_podname,
+ mysqldb_container_port=mysqldb_container_port,
+ mysqldb_name=mysqldb_name,
+ mysql_user=mysqldb_user,
+ mysql_password=mysqldb_password
)
- if tables_output and not found:
- found = tables_output
-
- # Fetch iDRAC IPs if table exists
- if found and not ip_output:
- query_ips = f"SELECT ip FROM {mysqldb_name}.services"
- ip_output = run_mysql_query_in_pod(
- telemetry_namespace,
- idrac_podname,
- mysqldb_k8s_name,
- mysqldb_user,
- mysqldb_password,
- query_ips
- )
- module.warn(f"iDRAC IPs output from {idrac_podname}: {ip_output}")
- if ip_output.get("rc") == 0:
- ip_list = ip_output.get("result", [])
+
+ if read_result.get("rc") == 0:
+ found = read_result.get("tables_found")
+ ip_list = read_result.get("ip_list", [])
module.warn(f"iDRAC IPs found in {idrac_podname}: {ip_list}")
break
diff --git a/common/vars/upgrade_vars.yml b/common/vars/upgrade_vars.yml
index 2c3669b4af..b290e66d4c 100644
--- a/common/vars/upgrade_vars.yml
+++ b/common/vars/upgrade_vars.yml
@@ -1,87 +1,87 @@
-# 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.
----
-
-# ============================================================================
-# OMNIA UPGRADE CONFIGURATION (Production Recommended)
-# ============================================================================
-# Upgrade metadata (source and target Omnia versions) is read automatically
-# from /opt/omnia/.data/oim_metadata.yml after omnia_core execution.
-#
-# Design:
-# omnia_upgrade_paths — defines the Omnia version upgrade sequence.
-# Each entry specifies the software versions for the NEXT Omnia version.
-#
-# components — defines each software component and its valid version sequence.
-# supported_versions — ordered list used for automatic intermediate hop
-# generation when a K8s version gap is detected.
-#
-# Automatic intermediate hop detection:
-# When the target K8s version skips one or more entries in supported_versions,
-# the system auto-generates one K8s hop per intermediate version.
-#
-# Example: Omnia 2.1.0.0 (K8s 1.34.1) → Omnia 2.3.0.0 (K8s 1.37.1)
-# Omnia path : 2.1.0.0 → 2.2.0.0 → 2.3.0.0
-# K8s hops : 1.34.1 → 1.35.1 (Omnia 2.1→2.2, direct)
-# 1.35.1 → 1.36.1 (auto-generated, within 2.2→2.3)
-# 1.36.1 → 1.37.1 (Omnia 2.2→2.3, final)
-# ============================================================================
-
-# ============================================================================
-# OMNIA VERSION UPGRADE PATHS
-# ============================================================================
-# Each entry:
-# "":
-# next_omnia_version: ""
-# software_versions:
-# : ""
-# ============================================================================
-omnia_upgrade_paths:
- "2.1.0.0":
- next_omnia_version: "2.2.0.0"
- software_versions:
- service_k8s: "1.35.1"
- # Uncomment to enable multi-hop upgrade to Omnia 2.3.0.0:
- # K8s 1.35.1 -> 1.37.1 will auto-generate intermediate hop via 1.36.1
- # "2.2.0.0":
- # next_omnia_version: "2.3.0.0"
- # software_versions:
- # service_k8s: "1.37.1"
-
-# ============================================================================
-# COMPONENT CONFIGURATION
-# ============================================================================
-# Each component:
-# json_file — base name for versioned JSON files
-# (e.g., "service_k8s" → service_k8s_v1.35.1.json)
-# enabled — whether this component participates in upgrade
-# supported_versions — ordered list of all valid software versions.
-# When target skips versions, intermediate hops are
-# auto-generated in sequence order.
-# ============================================================================
-components:
- service_k8s:
- json_file: "service_k8s"
- enabled: true
- supported_versions:
- - "1.34.1"
- - "1.35.1"
- # Additional components (placeholders)
- # slurm_custom:
- # json_file: "slurm_custom"
- # enabled: false
- # supported_versions:
- # - "24.05"
- # - "25.11"
- # - "26.05"
+# 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.
+---
+
+# ============================================================================
+# OMNIA UPGRADE CONFIGURATION (Production Recommended)
+# ============================================================================
+# Upgrade metadata (source and target Omnia versions) is read automatically
+# from /opt/omnia/.data/oim_metadata.yml after omnia_core execution.
+#
+# Design:
+# omnia_upgrade_paths — defines the Omnia version upgrade sequence.
+# Each entry specifies the software versions for the NEXT Omnia version.
+#
+# components — defines each software component and its valid version sequence.
+# supported_versions — ordered list used for automatic intermediate hop
+# generation when a K8s version gap is detected.
+#
+# Automatic intermediate hop detection:
+# When the target K8s version skips one or more entries in supported_versions,
+# the system auto-generates one K8s hop per intermediate version.
+#
+# Example: Omnia 2.1.0.0 (K8s 1.34.1) → Omnia 2.3.0.0 (K8s 1.37.1)
+# Omnia path : 2.1.0.0 → 2.2.0.1 → 2.3.0.0
+# K8s hops : 1.34.1 → 1.35.1 (Omnia 2.1→2.2, direct)
+# 1.35.1 → 1.36.1 (auto-generated, within 2.2→2.3)
+# 1.36.1 → 1.37.1 (Omnia 2.2→2.3, final)
+# ============================================================================
+
+# ============================================================================
+# OMNIA VERSION UPGRADE PATHS
+# ============================================================================
+# Each entry:
+# "":
+# next_omnia_version: ""
+# software_versions:
+# : ""
+# ============================================================================
+omnia_upgrade_paths:
+ "2.1.0.0":
+ next_omnia_version: "2.2.0.1"
+ software_versions:
+ service_k8s: "1.35.1"
+ # Uncomment to enable multi-hop upgrade to Omnia 2.3.0.0:
+ # K8s 1.35.1 -> 1.37.1 will auto-generate intermediate hop via 1.36.1
+ # "2.2.0.1":
+ # next_omnia_version: "2.3.0.0"
+ # software_versions:
+ # service_k8s: "1.37.1"
+
+# ============================================================================
+# COMPONENT CONFIGURATION
+# ============================================================================
+# Each component:
+# json_file — base name for versioned JSON files
+# (e.g., "service_k8s" → service_k8s_v1.35.1.json)
+# enabled — whether this component participates in upgrade
+# supported_versions — ordered list of all valid software versions.
+# When target skips versions, intermediate hops are
+# auto-generated in sequence order.
+# ============================================================================
+components:
+ service_k8s:
+ json_file: "service_k8s"
+ enabled: true
+ supported_versions:
+ - "1.34.1"
+ - "1.35.1"
+ # Additional components (placeholders)
+ # slurm_custom:
+ # json_file: "slurm_custom"
+ # enabled: false
+ # supported_versions:
+ # - "24.05"
+ # - "25.11"
+ # - "26.05"
diff --git a/omnia.sh b/omnia.sh
index 3cf8d6b17b..bb15a075c4 100755
--- a/omnia.sh
+++ b/omnia.sh
@@ -106,7 +106,7 @@ get_metadata_version() {
fi
}
-omnia_release=2.2.0.0
+omnia_release=2.2.0.1
omnia_prev_release=2.1.0.0
core_container_status=false
@@ -139,7 +139,7 @@ TARGET_CONTAINER_TAG="" # Target container tag for upgrade
# Note: Include RC milestones so upgrades from RC to RC/GA appear
ALL_OMNIA_VERSIONS=(
"2.1.0.0"
- "2.2.0.0"
+ "2.2.0.1"
)
# Container-side paths (used inside podman exec commands)
@@ -303,13 +303,13 @@ validate_container_image() {
echo -e "${BLUE}Build the required image using the following commands:${NC}"
echo ""
echo -e "git clone https://github.com/dell/omnia-containers.git -b omnia-container-"
- echo -e "${YELLOW}Note: Replace with the target Omnia version (e.g., v2.2.0.0)${NC}"
+ echo -e "${YELLOW}Note: Replace with the target Omnia version (e.g., v2.2.0.1)${NC}"
echo ""
echo -e "cd omnia-containers"
echo ""
echo -e "./build_images.sh core core_tag= omnia_branch="
- echo -e "${YELLOW}Note: Replace with the target Omnia branch (e.g., v2.2.0.0)${NC}"
- echo -e "${YELLOW}Note: core_tag will be the first 2 digits of the target Omnia version (e.g., 2.2 for v2.2.0.0)${NC}"
+ echo -e "${YELLOW}Note: Replace with the target Omnia branch (e.g., v2.2.0.1)${NC}"
+ echo -e "${YELLOW}Note: core_tag will be the first 2 digits of the target Omnia version (e.g., 2.2 for v2.2.0.1)${NC}"
echo ""
echo -e "${BLUE}After the image is built successfully, re-run:${NC}"
echo -e "./omnia.sh --$operation"
diff --git a/rollback/roles/rollback_openchami/tasks/main.yml b/rollback/roles/rollback_openchami/tasks/main.yml
index 725bc73b2a..a195748ad5 100644
--- a/rollback/roles/rollback_openchami/tasks/main.yml
+++ b/rollback/roles/rollback_openchami/tasks/main.yml
@@ -16,7 +16,7 @@
# ============================================================================
# rollback_openchami — Main Orchestration
# ============================================================================
-# Reverse the OpenCHAMI upgrade (2.2.0.0 → 2.1.0.0) by restoring:
+# Reverse the OpenCHAMI upgrade (2.2.0.1 → 2.1.0.0) by restoring:
# 1. Quadlet files and openchami.target from backup
# 2. v2.1 RPMs (openchami + ochami CLI) from backup
# 3. Configuration files (coredhcp.yaml, Corefile, configs_vars.yaml)
diff --git a/rollback/roles/rollback_openchami/vars/main.yml b/rollback/roles/rollback_openchami/vars/main.yml
index ed9c2dfde2..f5c111ddb4 100644
--- a/rollback/roles/rollback_openchami/vars/main.yml
+++ b/rollback/roles/rollback_openchami/vars/main.yml
@@ -16,7 +16,7 @@
# ============================================================================
# rollback_openchami — Variables
# ============================================================================
-# Rollback target: Omnia 2.1.0.0 (from Omnia 2.2.0.0)
+# Rollback target: Omnia 2.1.0.0 (from Omnia 2.2.0.1)
# Reverses the upgrade performed by upgrade_openchami role.
# ============================================================================
@@ -192,7 +192,7 @@ rollback_messages:
════════════════════════════════════════════
OPENCHAMI ROLLBACK COMPLETED SUCCESSFULLY
════════════════════════════════════════════
- Rolled back from Omnia 2.2.0.0 to Omnia 2.1.0.0.
+ Rolled back from Omnia 2.2.0.1 to Omnia 2.1.0.0.
All v2.1 containers are running. Database restored.
Cloud-init data reloaded from backup.
failure: |
diff --git a/telemetry/roles/idrac_telemetry/tasks/initiate_telemetry_service_cluster.yml b/telemetry/roles/idrac_telemetry/tasks/initiate_telemetry_service_cluster.yml
index 8a184d244b..44f1366eea 100644
--- a/telemetry/roles/idrac_telemetry/tasks/initiate_telemetry_service_cluster.yml
+++ b/telemetry/roles/idrac_telemetry/tasks/initiate_telemetry_service_cluster.yml
@@ -73,6 +73,7 @@
mysqldb_password: "{{ hostvars['localhost']['mysqldb_password'] }}"
db_retries: "{{ db_retries }}"
db_delay: "{{ db_delay }}"
+ mysqldb_container_port: "{{ mysqldb_container_port }}"
register: existing_mysqldb_idracips
rescue:
- name: Failed to connect mysqldb
@@ -181,6 +182,7 @@
auth_type: "{{ auth_type }}"
db_retries: "{{ db_retries }}"
db_delay: "{{ db_delay }}"
+ mysqldb_container_port: "{{ mysqldb_container_port }}"
register: add_idrac_to_db
rescue:
- name: Failed to connect mysqldb
diff --git a/telemetry/roles/idrac_telemetry/tasks/remove_deleted_nodes.yml b/telemetry/roles/idrac_telemetry/tasks/remove_deleted_nodes.yml
index 096133c587..c0f8d02207 100644
--- a/telemetry/roles/idrac_telemetry/tasks/remove_deleted_nodes.yml
+++ b/telemetry/roles/idrac_telemetry/tasks/remove_deleted_nodes.yml
@@ -70,6 +70,7 @@
pod_to_db_idrac_ips: "{{ existing_pod_to_db_idrac_ips }}"
db_retries: "{{ db_retries }}"
db_delay: "{{ db_delay }}"
+ mysqldb_container_port: "{{ mysqldb_container_port }}"
register: delete_idrac_result
rescue:
- name: Failed to delete iDRAC IPs from mysqldb
diff --git a/upgrade/roles/prep_local_repo/artifacts/repos.yml b/upgrade/roles/prep_local_repo/artifacts/repos.yml
index 7c9e94efcb..35feda28b1 100644
--- a/upgrade/roles/prep_local_repo/artifacts/repos.yml
+++ b/upgrade/roles/prep_local_repo/artifacts/repos.yml
@@ -2,7 +2,7 @@
# OMNIA VERSION-SPECIFIC REPOSITORIES FOR MULTI-HOP UPGRADES
# ============================================================================
# This file contains repositories organized by Omnia version to support:
-# - Multi-hop upgrades (e.g., 2.1.0.0 → 2.2.0.0 → 2.3.0.0)
+# - Multi-hop upgrades (e.g., 2.1.0.0 → 2.2.0.1 → 2.3.0.0)
# - Automatic intermediate K8s hop support
# - Future component additions (e.g., slurm in Omnia 2.3.0.0)
# - Rollback to any intermediate Omnia version
@@ -24,18 +24,18 @@
# input/local_repo_config.yml and are NOT duplicated here.
#
# Multi-Hop Examples:
-# 2.1.0.0 → 2.2.0.0 : Uses omnia_versions["2.2.0.0"] repos
-# 2.1.0.0 → 2.3.0.0 : Uses omnia_versions["2.2.0.0"] + omnia_versions["2.3.0.0"] repos
+# 2.1.0.0 → 2.2.0.1 : Uses omnia_versions["2.2.0.1"] repos
+# 2.1.0.0 → 2.3.0.0 : Uses omnia_versions["2.2.0.1"] + omnia_versions["2.3.0.0"] repos
# ============================================================================
# Omnia version-specific repositories
omnia_versions:
# --------------------------------------------------------------------------
- # Omnia 2.2.0.0 — K8s v1.35.1
+ # Omnia 2.2.0.1 — K8s v1.35.1
# K8s hop: 1.34.1 → 1.35.1 (direct, no intermediate hops)
# --------------------------------------------------------------------------
- "2.2.0.0":
+ "2.2.0.1":
omnia_repo_url_rhel_x86_64:
- url: "https://pkgs.k8s.io/core:/stable:/v1.35/rpm/"
gpgkey: "https://pkgs.k8s.io/core:/stable:/v1.35/rpm/repodata/repomd.xml.key"
diff --git a/upgrade/upgrade.yml b/upgrade/upgrade.yml
index c954063c7e..0f4d631e3a 100644
--- a/upgrade/upgrade.yml
+++ b/upgrade/upgrade.yml
@@ -385,7 +385,7 @@
── Version Transition ───────────────────────────────────────
Current Omnia version : {{ approval_manifest.source_version | default('2.1.0.0') }}
- Target Omnia version : {{ approval_manifest.target_version | default('2.2.0.0') }}
+ Target Omnia version : {{ approval_manifest.target_version | default('2.2.0.1') }}
── Upgrade Execution Plan (BuildStream Enabled) ─────────────
1. oim → Upgrade OpenCHAMI control-plane containers
@@ -449,7 +449,7 @@
── Version Transition ───────────────────────────────────────
Current Omnia version : {{ approval_manifest.source_version | default('2.1.0.0') }}
- Target Omnia version : {{ approval_manifest.target_version | default('2.2.0.0') }}
+ Target Omnia version : {{ approval_manifest.target_version | default('2.2.0.1') }}
── Omnia Upgrade Execution Plan (in order) ──────────────────
1. oim → Upgrade OpenCHAMI control-plane containers