From b81713c01d4fc676ab2858ca1b2c934a85cd4554 Mon Sep 17 00:00:00 2001 From: richardhenwood-dell Date: Tue, 1 Sep 2026 06:12:09 -0500 Subject: [PATCH 1/3] link to vuln report (#5092) Signed-off-by: richardhenwood-dell --- SECURITY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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. From cae92c9e19ac705a5fcf792af65bb57bc2abef7d Mon Sep 17 00:00:00 2001 From: richardhenwood-dell Date: Tue, 1 Sep 2026 06:13:07 -0500 Subject: [PATCH 2/3] link on main page explaining how to disclose a security vuln (#5093) land this after: https://github.com/dell/omnia/pull/5092 Signed-off-by: richardhenwood-dell Co-authored-by: Abhishek S A --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) 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). + +---- + From ff8f3bbd44238f91f1c2ada70cd630c896abee6e Mon Sep 17 00:00:00 2001 From: Kratika Patidar Date: Thu, 3 Sep 2026 18:25:08 +0530 Subject: [PATCH 3/3] Refactor MySQL Query Handling for iDRAC IP Retrieval (#5131) * mysql query update Signed-off-by: Kratika_Patidar * update delete idrac ips Signed-off-by: Kratika_Patidar * Delete common/library/modules/tests/test_idrac_mysql_modules.py This file not required Signed-off-by: priti-parate <140157516+priti-parate@users.noreply.github.com> * Remove unused import Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Signed-off-by: priti-parate <140157516+priti-parate@users.noreply.github.com> * upgrade version to change Signed-off-by: priti_parate * keep automation version same as v2.2.0.0 Signed-off-by: priti-parate <140157516+priti-parate@users.noreply.github.com> --------- Signed-off-by: Kratika_Patidar Signed-off-by: priti-parate <140157516+priti-parate@users.noreply.github.com> Signed-off-by: priti_parate Co-authored-by: priti-parate <140157516+priti-parate@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Co-authored-by: priti_parate --- .metadata/omnia_version | 2 +- .../upgrade/upgrade_hop_calculator_lib.py | 2 +- .../modules/delete_idracips_from_mysqldb.py | 139 +++++++------- .../modules/insert_idracips_mysqldb.py | 141 ++++++++------ .../modules/read_idracips_from_mysqldb.py | 179 ++++++++++-------- common/vars/upgrade_vars.yml | 174 ++++++++--------- omnia.sh | 10 +- .../roles/rollback_openchami/tasks/main.yml | 2 +- .../roles/rollback_openchami/vars/main.yml | 4 +- .../initiate_telemetry_service_cluster.yml | 2 + .../tasks/remove_deleted_nodes.yml | 1 + .../roles/prep_local_repo/artifacts/repos.yml | 10 +- upgrade/upgrade.yml | 4 +- 13 files changed, 355 insertions(+), 315 deletions(-) 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/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