diff --git a/config/inputs/io_veth_input.txt b/config/inputs/io_veth_input.txt index 3b92d2b8..cdc76ccd 100644 --- a/config/inputs/io_veth_input.txt +++ b/config/inputs/io_veth_input.txt @@ -6,7 +6,7 @@ netmask = 255.255.255.0 host_interfaces = "env4 env5" host_public_ip = "" peer_ips = "45.1.1.139 46.1.1.139" -peer_user = "" +peer_user = "root" peer_password = "" bond_interfaces = "env4 env5" host_ips = "45.1.1.27 46.1.1.27" @@ -21,7 +21,7 @@ sleep_time = 35 count = 10 EXPECTED_THROUGHPUT = 50 UPERF_SERVER_RUN = 1 -TIMEOUT = "300" +TIMEOUT = 300 NETSERVER_RUN = 1 duration = 600 minimum_iterations = 1 @@ -34,10 +34,10 @@ drop_accepted = 10 nmap_download = "https://nmap.org/dist/nmap-7.80.tar.bz2" bridge_interface = "br0" hmc_pwd = "" -hmc_username = "" +hmc_username = "hscroot" num_of_dlpar = 20 vios_ip = "" -vios_username = "" +vios_username = "padmin" vios_pwd = "" module = "ibmveth" iteration = 20 @@ -47,7 +47,7 @@ function = 4 err = 1 htx_host_interfaces = "env4" net_ids = '45' -htx_rpm_link = "" +htx_rpm_link = "https://rchgsa.ibm.com:7191/gsa/rchgsa/projects/h/htx/public_html/htxonly/" hbond = False ip_config = "True" mtu_timeout = 30 diff --git a/config/inputs/io_vnic_input.txt b/config/inputs/io_vnic_input.txt index 1afa4e6a..8966ee0d 100755 --- a/config/inputs/io_vnic_input.txt +++ b/config/inputs/io_vnic_input.txt @@ -34,7 +34,7 @@ count = 5 peer_user_name = root EXPECTED_THROUGHPUT = 80 UPERF_SERVER_RUN = 1 -TIMEOUT = "300" +TIMEOUT = 300 NETSERVER_RUN = 1 duration = 600 minimum_iterations = 1 @@ -57,3 +57,4 @@ run_type = 'rpm' mtu_timeout = 30 manageSystem = "" sriov = 'no' +htx_rpm_link = "https://rchgsa.ibm.com:7191/gsa/rchgsa/projects/h/htx/public_html/htxonly/" \ No newline at end of file diff --git a/config/wrapper/pci_input.conf b/config/wrapper/pci_input.conf index 5321185c..56a2fe3b 100644 --- a/config/wrapper/pci_input.conf +++ b/config/wrapper/pci_input.conf @@ -120,6 +120,19 @@ host_public_ip = public_interface_ip interface = interfaces:0 module = driver macaddress = macaddress +peer_ip = peer_ip +peer_ips = peer_ips +peer_interfaces = peer_interfaces +peer_public_ip = peer_public_ip +peer_password = peer_password +host_ip = host_ip +host_ips = host_ips +netmask = netmask +netmasks = netmasks +manageSystem = manageSystem +vios_names = vios_names +vios_ip = vios_ip +device_ip = host_ip [io_veth_fvt] host_interfaces = interfaces:all @@ -130,6 +143,19 @@ host_public_ip = public_interface_ip interface = interfaces:0 module = driver macaddress = macaddress +peer_ip = peer_ip +peer_ips = peer_ips +peer_interfaces = peer_interfaces +peer_public_ip = peer_public_ip +peer_password = peer_password +host_ip = host_ip +host_ips = host_ips +netmask = netmask +netmasks = netmasks +manageSystem = manageSystem +vios_names = vios_names +vios_ip = vios_ip +device_ip = host_ip [io_hnv_fvt] host_interfaces = interfaces:all @@ -140,6 +166,19 @@ host_public_ip = public_interface_ip interface = interfaces:0 module = driver macaddress = macaddress +peer_ip = peer_ip +peer_ips = peer_ips +peer_interfaces = peer_interfaces +peer_public_ip = peer_public_ip +peer_password = peer_password +host_ip = host_ip +host_ips = host_ips +netmask = netmask +netmasks = netmasks +manageSystem = manageSystem +vios_names = vios_names +vios_ip = vios_ip +device_ip = host_ip [io_ib_fvt] interface = interfaces:0 diff --git a/lib/helper.py b/lib/helper.py index 109d58f3..bcd87f61 100644 --- a/lib/helper.py +++ b/lib/helper.py @@ -18,6 +18,7 @@ import os import re import sys +import shlex import shutil import stat import platform @@ -264,3 +265,117 @@ def uninstall(self): runcmd(cmd, ignore_status=True, err_str="Error in removing package: %s" % package, debug_str="Uninstalling %s" % package) + + + +class RemoteRunner: + """ + SSH-based remote command runner using ``sshpass`` + the system ``ssh`` + binary. No Python C-extension dependencies (no paramiko/bcrypt/pynacl) + are required — only the ``sshpass`` package must be installed on the + *local* machine (``yum install sshpass`` / ``apt-get install sshpass``). + + Provides the same ``runcmd()`` interface as the local helper so it can be + used as a drop-in replacement for remote machines. + + Usage:: + + runner = RemoteRunner(host='192.168.1.10', username='root', password='secret') + status, output = runner.runcmd('ip a s dev eth0') + runner.close() # no-op for this implementation, kept for API compat + + Context-manager usage:: + + with RemoteRunner(host='192.168.1.10', username='root', password='secret') as r: + status, output = r.runcmd('lsdevinfo -c') + """ + + # Common SSH options that suppress host-key prompts and banners. + _SSH_OPTS = ( + "-o StrictHostKeyChecking=no " + "-o UserKnownHostsFile=/dev/null " + "-o BatchMode=no " + "-o LogLevel=ERROR" + ) + + def __init__(self, host, username, password, port=22, timeout=30): + """ + Store connection parameters and verify that ``sshpass`` is available. + + :param host: Hostname or IP address of the remote machine. + :param username: SSH login username. + :param password: SSH login password. + :param port: SSH port (default 22). + :param timeout: Per-command connect timeout in seconds (default 30). + """ + # Verify sshpass is on PATH + chk_status, _ = subprocess.getstatusoutput("which sshpass") + if chk_status != 0: + logger.error( + "sshpass is not installed. Install it with: " + "yum install sshpass OR apt-get install sshpass" + ) + sys.exit(1) + + self.host = host + self.username = username + self._password = password + self.port = port + self.timeout = timeout + logger.info("RemoteRunner ready for %s@%s:%s (sshpass mode)", username, host, port) + + def runcmd(self, cmd, ignore_status=False, err_str="", info_str="", debug_str=""): + """ + Run *cmd* on the remote host via ``sshpass``/``ssh``. + + :param cmd: Shell command string to execute remotely. + :param ignore_status: If False (default), calls sys.exit(1) on non-zero exit. + :param err_str: Message to log at ERROR level on failure. + :param info_str: Message to log at INFO level before running. + :param debug_str: Message to log at DEBUG level before running. + :return: (status, output) tuple — identical contract to local runcmd(). + """ + if info_str: + logger.info(info_str) + if debug_str: + logger.debug(debug_str) + + # Build: sshpass -p ssh -p -o ConnectTimeout= user@host '' + remote_cmd = ( + "sshpass -p {pwd} ssh {opts} -p {port} " + "-o ConnectTimeout={timeout} " + "{user}@{host} {quoted_cmd}" + ).format( + pwd=shlex.quote(self._password), + opts=self._SSH_OPTS, + port=self.port, + timeout=self.timeout, + user=self.username, + host=self.host, + quoted_cmd=shlex.quote(cmd), + ) + + logger.debug("Remote(%s) running: %s", self.host, cmd) + try: + status, output = subprocess.getstatusoutput(remote_cmd) + if status != 0 and not ignore_status: + if err_str: + logger.error("%s %s", err_str, output) + sys.exit(1) + logger.debug(output) + return (status, output) + except Exception as error: + if err_str: + logger.error("%s %s", err_str, error) + sys.exit(1) + + def close(self): + """No persistent connection to close; kept for API compatibility.""" + logger.debug("RemoteRunner.close() called for %s (no-op)", self.host) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + return False diff --git a/lib/hmc.py b/lib/hmc.py new file mode 100644 index 00000000..3c288e00 --- /dev/null +++ b/lib/hmc.py @@ -0,0 +1,460 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# See LICENSE for more details. +# +# Copyright: 2024 IBM +# Author: Shaik Abdulla + +""" +HMC CLI helper module. + +Connects to an IBM HMC over SSH (using sshpass) and runs HMC CLI commands +to discover managed systems and LPAR details. + +Typical usage +------------- + from lib.hmc import HMCClient + + with HMCClient(hmc_ip='192.168.1.100', username='hscroot', password='abc123') as hmc: + # List all managed systems on this HMC + systems = hmc.list_managed_systems() + print(systems) + # ['Server-9009-42A-SN12345', 'Server-9009-42A-SN67890'] + + # Find which managed system a given LPAR name belongs to + managed_system = hmc.get_managed_system_for_lpar('my-lpar-01') + print(managed_system) + # 'Server-9009-42A-SN12345' + + # Get full details of a specific LPAR + info = hmc.get_lpar_info('my-lpar-01', managed_system) + print(info) +""" + +import os +import re +import subprocess +import shlex + +try: + import requests + _REQUESTS_AVAILABLE = True +except ImportError: + _REQUESTS_AVAILABLE = False + +from lib.logger import logger_init + +BASE_PATH = os.path.dirname(os.path.abspath(os.path.join(__file__, os.pardir))) +logger = logger_init(filepath=BASE_PATH).getlogger() + +# SSH options that suppress host-key prompts and banners — same as RemoteRunner +_SSH_OPTS = ( + "-o StrictHostKeyChecking=no " + "-o UserKnownHostsFile=/dev/null " + "-o BatchMode=no " + "-o LogLevel=ERROR" +) + + +def get_hmc_ip_from_lsrsrc(): + """ + Detect the HMC IP address from the local RSCT resource class ``IBM.MCP``. + + Runs ``lsrsrc IBM.MCP IPAddresses`` and extracts the first IP address + from the ``IPAddresses`` attribute, which holds the HMC management IP(s). + + :return: HMC IP string, or None if not found / RSCT not available. + """ + status, output = subprocess.getstatusoutput( + "lsrsrc IBM.MCP IPAddresses 2>/dev/null" + ) + if status != 0 or not output.strip(): + logger.warning("lsrsrc IBM.MCP IPAddresses returned no output (RSCT not available?)") + return None + + # Output looks like: + # resource 1: + # IPAddresses = {"192.168.1.100","192.168.1.101"} + # Extract the first IP inside the braces / quotes + match = re.search(r'IPAddresses\s*=\s*\{?"?(\d{1,3}(?:\.\d{1,3}){3})', output) + if match: + hmc_ip = match.group(1) + logger.info("HMC IP detected via lsrsrc IBM.MCP: %s", hmc_ip) + return hmc_ip + + logger.warning("Could not parse IPAddresses from lsrsrc IBM.MCP output:\n%s", output) + return None + + +def get_hmc_password_from_secrets_manager(url='https://web.stg-secrets-manager.dal.app.cirrus.ibm.com/'): + """ + Fetch the current HMC/lab password from the IBM Cirrus Secrets Manager web page. + + The page is a JavaScript SPA; this function attempts to retrieve the + rendered content by: + 1. Fetching the SPA shell to extract the API base URL / auth token hints. + 2. Trying common REST API endpoints that SPAs typically expose. + 3. Parsing the response for a "Lab Password History" section and + returning the first (most recent) password entry. + + If the page cannot be fetched or parsed, returns None and logs a warning + so the caller can fall back to a manually supplied password. + + :param url: Base URL of the secrets manager (default: IBM Cirrus staging). + :return: Password string, or None if unavailable. + """ + if not _REQUESTS_AVAILABLE: + logger.warning( + "requests library not installed. Install with: pip3 install requests\n" + "Cannot auto-fetch HMC password from secrets manager." + ) + return None + + session = requests.Session() + session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (compatible; pci_info/1.0)', + 'Accept': 'application/json, text/html, */*', + }) + + # ---- Step 1: fetch the SPA shell ---- + try: + resp = session.get(url, timeout=15, verify=False) + resp.raise_for_status() + except Exception as err: + logger.warning("Could not reach secrets manager at %s: %s", url, err) + return None + + html = resp.text + + # ---- Step 2: try to find an embedded API base or JS bundle with password data ---- + # Some SPAs embed their API base URL in the HTML or in a config JS file. + api_base_match = re.search(r'(https?://[^\s"\']+/api)', html) + api_base = api_base_match.group(1) if api_base_match else url.rstrip('/') + '/api' + + # Common REST endpoints for lab/password resources + candidate_paths = [ + '/lab-passwords', + '/passwords', + '/lab/passwords', + '/secrets', + '/lab-password-history', + ] + + for path in candidate_paths: + try: + api_url = api_base.rstrip('/') + path + api_resp = session.get(api_url, timeout=10, verify=False) + if api_resp.status_code == 200: + data = api_resp.json() + # Handle list response — take first entry + if isinstance(data, list) and data: + entry = data[0] + # Common field names for the password value + for field in ('password', 'passwd', 'value', 'secret', 'lab_password'): + if field in entry: + logger.info("HMC password retrieved from %s", api_url) + return str(entry[field]) + # Handle dict response with a list inside + if isinstance(data, dict): + for key in ('results', 'data', 'passwords', 'items'): + if key in data and isinstance(data[key], list) and data[key]: + entry = data[key][0] + for field in ('password', 'passwd', 'value', 'secret', 'lab_password'): + if field in entry: + logger.info("HMC password retrieved from %s (key=%s)", api_url, key) + return str(entry[field]) + except Exception: + continue + + # ---- Step 3: last resort — regex scan the raw HTML for password-like values + # near "Lab Password History" + lab_section = re.search( + r'Lab Password History.*?([A-Za-z0-9!@#$%^&*()_+\-=]{8,})', + html, re.DOTALL | re.IGNORECASE + ) + if lab_section: + password = lab_section.group(1).strip() + logger.info("HMC password extracted from HTML near 'Lab Password History'") + return password + + logger.warning( + "Could not extract HMC password from %s. " + "The page may require browser-based JavaScript rendering. " + "Please supply the password manually via --hmc-password.", + url + ) + return None + + +class HMCClient: + """ + Thin SSH wrapper around the HMC CLI. + + All HMC CLI commands are executed via ``sshpass`` + ``ssh`` so no + Python C-extension dependencies are required. Only ``sshpass`` must + be installed on the local machine:: + + yum install sshpass # RHEL/CentOS/Fedora + apt-get install sshpass # Ubuntu/Debian + + Parameters + ---------- + hmc_ip : str + Hostname or IP address of the HMC. + username : str + HMC login username (default HMC admin user is ``hscroot``). + password : str + HMC login password. + port : int + SSH port on the HMC (default 22). + timeout : int + Per-command connect timeout in seconds (default 30). + """ + + def __init__(self, hmc_ip, username='hscroot', password='', port=22, timeout=30): + # Verify sshpass is available + chk, _ = subprocess.getstatusoutput("which sshpass") + if chk != 0: + logger.error( + "sshpass is not installed. Install it with: " + "yum install sshpass OR apt-get install sshpass" + ) + raise RuntimeError("sshpass not found in PATH") + + self.hmc_ip = hmc_ip + self.username = username + self._password = password + self.port = port + self.timeout = timeout + logger.info("HMCClient ready for %s@%s:%s", username, hmc_ip, port) + + # ------------------------------------------------------------------ # + # Internal helpers + # ------------------------------------------------------------------ # + + def _run(self, hmc_cmd, ignore_status=False): + """ + Execute *hmc_cmd* on the HMC via sshpass/ssh. + + :param hmc_cmd: HMC CLI command string (e.g. ``lssyscfg -r sys -F name``). + :param ignore_status: When True, non-zero exit does not raise/exit. + :return: (status, output) tuple. + """ + remote_cmd = ( + "sshpass -p {pwd} ssh {opts} -p {port} " + "-o ConnectTimeout={timeout} " + "{user}@{host} {quoted_cmd}" + ).format( + pwd=shlex.quote(self._password), + opts=_SSH_OPTS, + port=self.port, + timeout=self.timeout, + user=self.username, + host=self.hmc_ip, + quoted_cmd=shlex.quote(hmc_cmd), + ) + logger.debug("HMC(%s) running: %s", self.hmc_ip, hmc_cmd) + status, output = subprocess.getstatusoutput(remote_cmd) + if status != 0 and not ignore_status: + logger.error("HMC command failed (exit %s): %s\n%s", status, hmc_cmd, output) + logger.debug(output) + return (status, output) + + # ------------------------------------------------------------------ # + # Public API + # ------------------------------------------------------------------ # + + def list_managed_systems(self): + """ + Return a list of all managed system names visible from this HMC. + + Uses: ``lssyscfg -r sys -F name`` + + :return: list of str, e.g. ``['Server-9009-42A-SN12345', ...]`` + """ + status, output = self._run("lssyscfg -r sys -F name", ignore_status=True) + logger.debug("list_managed_systems raw output (exit=%s): %r", status, output) + if status != 0 or not output.strip(): + logger.warning( + "No managed systems found on HMC %s (exit=%s). " + "Check HMC credentials, user permissions, and that the HMC manages at least one system. " + "Raw output: %r", + self.hmc_ip, status, output, + ) + return [] + systems = [s.strip() for s in output.splitlines() if s.strip()] + logger.info("Managed systems on %s: %s", self.hmc_ip, systems) + return systems + + def list_lpars(self, managed_system): + """ + Return a list of LPAR names on *managed_system*. + + Uses: ``lssyscfg -r lpar -m -F name`` + + :param managed_system: Managed system name as returned by + :meth:`list_managed_systems`. + :return: list of str LPAR names. + """ + cmd = "lssyscfg -r lpar -m %s -F name" % shlex.quote(managed_system) + status, output = self._run(cmd) + if status != 0 or not output.strip(): + logger.warning("No LPARs found on managed system %s", managed_system) + return [] + lpars = [l.strip() for l in output.splitlines() if l.strip()] + logger.info("LPARs on %s: %s", managed_system, lpars) + return lpars + + def get_managed_system_for_lpar(self, lpar_name): + """ + Find which managed system a given LPAR belongs to by iterating + over all managed systems and checking their LPAR lists. + + :param lpar_name: LPAR name to search for. + :return: managed system name (str) or None if not found. + """ + for system in self.list_managed_systems(): + if lpar_name in self.list_lpars(system): + logger.info("LPAR '%s' found on managed system '%s'", lpar_name, system) + return system + logger.warning("LPAR '%s' not found on any managed system of HMC %s", + lpar_name, self.hmc_ip) + return None + + def get_lpar_info(self, lpar_name, managed_system): + """ + Return a dict of key LPAR attributes for *lpar_name* on + *managed_system*. + + Uses: ``lssyscfg -r lpar -m --filter lpar_names=`` + + :param lpar_name: LPAR name. + :param managed_system: Managed system name. + :return: dict with keys such as ``name``, ``lpar_id``, ``state``, + ``lpar_env``, ``default_profile``, ``os_version``. + """ + fields = "name,lpar_id,state,lpar_env,default_profile,os_version" + cmd = ( + "lssyscfg -r lpar -m {ms} --filter lpar_names={lpar} -F {fields}" + ).format( + ms=shlex.quote(managed_system), + lpar=shlex.quote(lpar_name), + fields=fields, + ) + status, output = self._run(cmd) + if status != 0 or not output.strip(): + logger.warning("Could not retrieve info for LPAR '%s'", lpar_name) + return {} + values = output.strip().split(',') + keys = fields.split(',') + info = dict(zip(keys, values)) + logger.info("LPAR info for '%s': %s", lpar_name, info) + return info + + def get_vios_info(self, managed_system): + """ + Return VIOS names and their RMC IP addresses on *managed_system*. + + Step 1: ``lssyscfg -r lpar -m -F name,lpar_env --filter lpar_env=vioserver`` + to get VIOS partition names (confirmed working format: "ltcden7-vios1,vioserver"). + Step 2: For each VIOS name, fetch its RMC IP via a separate + ``lssyscfg -r lpar -m --filter lpar_names= -F rmc_ipaddr`` call. + + :param managed_system: Managed system name. + :return: dict with keys: + - ``vios_names`` — space-separated VIOS partition names + - ``vios_ip`` — space-separated VIOS RMC IP addresses + - ``vios_list`` — list of dicts, each with ``name`` and ``ip`` + """ + # Step 1: fetch ALL LPARs with name + lpar_env, then filter in Python. + # The HMC --filter flag can be unreliable across firmware versions; + # Python-side filtering on lpar_env containing "vio" (case-insensitive) + # or partition name containing "vios" is more robust. + cmd = ( + "lssyscfg -r lpar -m {ms} -F name,lpar_env" + ).format(ms=shlex.quote(managed_system)) + status, output = self._run(cmd, ignore_status=True) + if status != 0 or not output.strip(): + logger.warning("lssyscfg returned no output for managed system '%s'", managed_system) + return {'vios_names': '', 'vios_ip': '', 'vios_list': []} + + vios_list = [] + for line in output.strip().splitlines(): + line = line.strip() + if not line: + continue + # Format: "ltcden7-vios1,vioserver" + parts = line.split(',') + name = parts[0].strip() + lpar_env = parts[1].strip() if len(parts) > 1 else '' + # Match on lpar_env containing "vio" OR partition name containing "vios" + if ('vio' in lpar_env.lower() or 'vios' in name.lower()) and name: + vios_list.append({'name': name, 'ip': ''}) + + if not vios_list: + logger.warning("No VIOS partitions identified on managed system '%s'", managed_system) + return {'vios_names': '', 'vios_ip': '', 'vios_list': []} + + # Step 2: fetch RMC IP for each VIOS + for vios in vios_list: + ip_cmd = ( + "lssyscfg -r lpar -m {ms} --filter lpar_names={name} -F rmc_ipaddr" + ).format( + ms=shlex.quote(managed_system), + name=shlex.quote(vios['name']), + ) + ip_status, ip_output = self._run(ip_cmd, ignore_status=True) + if ip_status == 0 and ip_output.strip(): + vios['ip'] = ip_output.strip().split('\n')[0].strip() + + names = [v['name'] for v in vios_list] + # If only one VIOS, repeat it twice so callers always get two entries + if len(names) == 1: + names = names * 2 + vios_names = ' '.join(names) + vios_ip = ' '.join(v['ip'] for v in vios_list if v['ip']) + logger.info("VIOS on '%s': names=%s ips=%s", managed_system, vios_names, vios_ip) + return {'vios_names': vios_names, 'vios_ip': vios_ip, 'vios_list': vios_list} + + def get_managed_system_details(self, managed_system): + """ + Return a dict of key attributes for *managed_system*. + + Uses: ``lssyscfg -r sys -m -F name,serial_num,type_model,state`` + + :param managed_system: Managed system name. + :return: dict with keys ``name``, ``serial_num``, ``type_model``, ``state``. + """ + fields = "name,serial_num,type_model,state" + cmd = "lssyscfg -r sys -m {ms} -F {fields}".format( + ms=shlex.quote(managed_system), + fields=fields, + ) + status, output = self._run(cmd) + if status != 0 or not output.strip(): + logger.warning("Could not retrieve details for managed system '%s'", managed_system) + return {} + values = output.strip().split(',') + keys = fields.split(',') + details = dict(zip(keys, values)) + logger.info("Managed system details for '%s': %s", managed_system, details) + return details + + # ------------------------------------------------------------------ # + # Context manager support + # ------------------------------------------------------------------ # + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + # No persistent SSH connection to close (sshpass spawns per-command) + logger.debug("HMCClient context exited for %s", self.hmc_ip) + return False diff --git a/lib/virtual.py b/lib/virtual.py index df721082..132af1e9 100644 --- a/lib/virtual.py +++ b/lib/virtual.py @@ -14,6 +14,22 @@ """ Module for all Virtual devices related functions. + +All public functions accept an optional `runner` keyword argument. +When omitted (or None) the local `runcmd()` helper is used. +When a :class:`lib.helper.RemoteRunner` instance is passed, every shell +command is executed on the remote machine over SSH instead, enabling +remote interface discovery without any other code changes. + +Example — local (default behaviour, unchanged): + from lib import virtual + info = virtual.virtual_info('eth0') + +Example — remote: + from lib.helper import RemoteRunner + from lib import virtual + with RemoteRunner(host='192.168.1.10', username='root', password='s3cr3t') as r: + info = virtual.virtual_info('eth0', runner=r) """ import re @@ -25,15 +41,32 @@ BASE_PATH = os.path.dirname(os.path.abspath(__file__)) logger = logger_init(filepath=BASE_PATH).getlogger() -def get_mac_address(interface): + +def _run(cmd, runner=None, ignore_status=False): + """ + Internal helper: dispatch *cmd* to either the local runcmd() or a + RemoteRunner instance. + + :param cmd: Shell command string to execute. + :param runner: Optional RemoteRunner instance. Uses local runcmd when None. + :param ignore_status: Passed through to the underlying runner. + :return: (status, output) tuple. + """ + if runner is not None: + return runner.runcmd(cmd, ignore_status=ignore_status) + return runcmd(cmd, ignore_status=ignore_status) + + +def get_mac_address(interface, runner=None): ''' Gets the mac_address of given interface. - parameter: interface: Name of Interface. + :param interface: Name of Interface. + :param runner: Optional RemoteRunner for remote execution. :return: string of MAC address. ''' try: - for line in runcmd("ip a s dev %s" % interface)[1].splitlines(): + for line in _run("ip a s dev %s" % interface, runner=runner)[1].splitlines(): if 'link/ether' in line: mac_address = line.split()[1] return mac_address @@ -41,30 +74,31 @@ def get_mac_address(interface): logger.debug(f'Interface not found {e}') -def get_driver(interface): +def get_driver(interface, runner=None): ''' Gets associated driver/module of given interface. - parameter: interface: Name of Interface. + :param interface: Name of Interface. + :param runner: Optional RemoteRunner for remote execution. :return: string of driver name. ''' - for line in runcmd("ethtool -i %s" % interface)[1].splitlines(): + for line in _run("ethtool -i %s" % interface, runner=runner)[1].splitlines(): if line.startswith('driver:'): driver = line.split(': ')[1].strip() return driver -def get_virtual_interface_names(interface_type): +def get_virtual_interface_names(interface_type, runner=None): ''' Gets all virtual interface names of a given type. - :param interface_type: Type of interface (e.g. 'l-lan', 'vnic') + :param runner: Optional RemoteRunner for remote execution. :return: list of virtual interface names. ''' try: interface_list = [] - for input_string in runcmd("lsdevinfo -c")[1].splitlines(): + for input_string in _run("lsdevinfo -c", runner=runner)[1].splitlines(): if interface_type in input_string: pattern = r'name="([^"]+)"' match = re.search(pattern, input_string) @@ -78,38 +112,68 @@ def get_virtual_interface_names(interface_type): logger.debug(f"Error while getting interface list {e}") -def get_veth_interface_names(): - return get_virtual_interface_names('l-lan') +def get_veth_interface_names(runner=None): + return get_virtual_interface_names('l-lan', runner=runner) -def get_vnic_interface_names(): - return get_virtual_interface_names('vnic') +def get_vnic_interface_names(runner=None): + return get_virtual_interface_names('vnic', runner=runner) -def get_hnv_interface_names(): +def get_hnv_interface_names(runner=None): ''' Gets all HNV interface names. + :param runner: Optional RemoteRunner for remote execution. :return: list of HNV interface names. ''' hnv_interface_list = [] - bonding_dir = '/proc/net/bonding/' - if os.path.exists(bonding_dir): - bond_interfaces = os.listdir(bonding_dir) - hnv_interface_list.extend(bond_interfaces) + if runner is not None: + # On a remote host we cannot use os.path / os.listdir directly; + # instead we list the bonding proc directory via SSH. + status, output = _run("ls /proc/net/bonding/ 2>/dev/null", runner=runner, ignore_status=True) + if status == 0 and output.strip(): + hnv_interface_list.extend(output.strip().splitlines()) + else: + logger.debug("No HNV interfaces found on remote host.") else: - logger.debug("No HNV interfaces found.") + bonding_dir = '/proc/net/bonding/' + if os.path.exists(bonding_dir): + bond_interfaces = os.listdir(bonding_dir) + hnv_interface_list.extend(bond_interfaces) + else: + logger.debug("No HNV interfaces found.") return hnv_interface_list -def get_host_public_ip(): +def get_interface_ip(interface, runner=None): + ''' + Gets the IPv4 address assigned to a given interface. + + :param interface: Name of the network interface. + :param runner: Optional RemoteRunner for remote execution. + :return: string of IPv4 address, or None if not found. + ''' + try: + lines = _run("ip a s dev %s" % interface, runner=runner, ignore_status=True)[1] + ip_pattern = r'inet\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' + match = re.search(ip_pattern, lines) + if match: + return match.group(1) + except Exception as e: + logger.debug(f'Could not get IP for interface {interface}: {e}') + return None + + +def get_host_public_ip(runner=None): ''' Gets system's Public IP address. + :param runner: Optional RemoteRunner for remote execution. :return: string of Public IP address. ''' try: - lines = runcmd("ip a s dev net0")[1] + lines = _run("ip a s dev net0", runner=runner)[1] ip_pattern = r'inet\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' match = re.search(ip_pattern, lines) if match: @@ -118,30 +182,33 @@ def get_host_public_ip(): logger.debug(f'Interface not found {e}') -def virtual_info(interface): +def virtual_info(interface, runner=None): ''' Get the information for given virtual interface. - parameter: interface: Name of Interface. - :return: list of dictinaries of virtual interface information. + :param interface: Name of Interface. + :param runner: Optional RemoteRunner instance. When provided, all + underlying commands are executed on the remote host + over SSH instead of locally. + :return: list of dictionaries of virtual interface information. ''' virtual_list = [] virtual_dict = {} - virtual_dict['macaddress'] = get_mac_address(interface) - virtual_dict['public_interface_ip'] = get_host_public_ip() - virtual_dict['driver'] = get_driver(interface) + virtual_dict['macaddress'] = get_mac_address(interface, runner=runner) + virtual_dict['public_interface_ip'] = get_host_public_ip(runner=runner) + virtual_dict['driver'] = get_driver(interface, runner=runner) if virtual_dict['driver'] == "ibmvnic": - virtual_dict['interfaces'] = get_vnic_interface_names() + virtual_dict['interfaces'] = get_vnic_interface_names(runner=runner) virtual_dict['adapter_type'] = 'vnic' if virtual_dict['driver'] == "ibmveth": - virtual_dict['interfaces'] = get_veth_interface_names() + virtual_dict['interfaces'] = get_veth_interface_names(runner=runner) virtual_dict['adapter_type'] = 'veth' if virtual_dict['driver'] == "bonding": - virtual_dict['interfaces'] = get_hnv_interface_names() + virtual_dict['interfaces'] = get_hnv_interface_names(runner=runner) virtual_dict['adapter_type'] = 'hnv' virtual_list.append(virtual_dict) - return virtual_list + return virtual_list \ No newline at end of file diff --git a/pci_info.py b/pci_info.py old mode 100755 new mode 100644 index f71b996f..eeced534 --- a/pci_info.py +++ b/pci_info.py @@ -14,7 +14,7 @@ # Copyright: 2023 IBM # Author: Narasimhan V # Author: Manvanthara Puttashankar -# Author: Shaik Abdulla +# Author: Shaik Abdulla from pprint import pprint from lib import pci @@ -26,18 +26,20 @@ import configparser from lib.pci import is_sriov from lib.logger import logger_init -from lib.helper import is_rhel8 +from lib.helper import is_rhel8, RemoteRunner +from lib.hmc import HMCClient, get_hmc_ip_from_lsrsrc, get_hmc_password_from_secrets_manager +from typing import Optional BASE_PATH = os.path.dirname(os.path.abspath(__file__)) CONFIG_PATH = "%s/config/wrapper/pci_input.conf" % BASE_PATH -CONFIGFILE = configparser.SafeConfigParser() -CONFIGFILE.optionxform = str +CONFIGFILE = configparser.ConfigParser() +CONFIGFILE.optionxform = str # type: ignore[assignment] CONFIGFILE.read(CONFIG_PATH) BASE_INPUTFILE_PATH = "%s/config/inputs" % BASE_PATH input_path = "io_input.txt" INPUTFILE = configparser.ConfigParser() -INPUTFILE.optionxform = str -args = None +INPUTFILE.optionxform = str # type: ignore[assignment] +args: Optional[argparse.Namespace] = None logger = logger_init(filepath=BASE_PATH).getlogger() @@ -60,6 +62,7 @@ def create_config_inputs(orig_cfg, new_cfg, inputfile, interface, config_type): input_file_string :: A string, with extension of "--input-file" option. input_params:: A list, of different input parameters of specific interface. """ + assert args is not None, "args must be initialized before calling create_config_inputs" test_suites = [] input_file_string = "" input_params = [] @@ -67,29 +70,8 @@ def create_config_inputs(orig_cfg, new_cfg, inputfile, interface, config_type): if len(additional_params) != 0: additional_params = args.add_params.split(",") - # Exclude input parameters when vNIC has been created manually, as these parameters are not needed. - # exclude_inputs_params = ["manageSystem","sriov", "hmc_pwd", "hmc_username", "vios_ip","vios_username", - # "vios_pwd","slot_num", "vios_names","sriov_adapters", "sriov_ports", - # "priority", "auto_failover", "bandwidth"] - - # Exclude test-cases from test bucket when vNIC created manually. - exclude_test_cases = ["NetworkVirtualization.test_add", "NetworkVirtualization.test_backingdevadd", - "NetworkVirtualization.test_backingdevremove", "NetworkVirtualization.test_remove"] - - # when vNIC created manually, Read the orignal configuration file and write only required test-cases in new configuration file. - if config_type == 'vnic': - test_cases = [] - with open("config/tests/host/%s.cfg" % orig_cfg, 'r') as file: - - lines = file.readlines() - for testcases in lines: - if not any(exclude_test_case in testcases for exclude_test_case in exclude_test_cases): - test_cases.append(testcases) - - with open("config/tests/host/%s.cfg" % new_cfg, 'w+') as output_file: - output_file.write("".join(test_cases)) - else: - shutil.copy("config/tests/host/%s.cfg" % orig_cfg, "config/tests/host/%s.cfg" % new_cfg) + # Copy configuration file + shutil.copy("config/tests/host/%s.cfg" % orig_cfg, "config/tests/host/%s.cfg" % new_cfg) test_suites.append("host_%s" % new_cfg) @@ -147,29 +129,23 @@ def create_config_inputs(orig_cfg, new_cfg, inputfile, interface, config_type): except: pass - # exclude input parameters when vNIC has been created manually. - # if config_type == 'vnic': - # for key in list(inputfile_dict.keys()): - # if any(key.startswith(exclude) for exclude in exclude_inputs_params): - # del inputfile_dict[key] - # additional params for param in additional_params: - key = param.split('=')[0] + key = param.split('=')[0].strip() # handling additional params per pci if '::' in key: pci_root = key.split('::')[0].split('.')[0] - if pci_root != pci['pci_root']: + if pci_root != interface.get('pci_root', ''): continue key = key.split('::')[1] # check if the newly added additional param is same # as inputfile assign the values directly if key in inputfile_dict: - inputfile_dict[key] = param.split('=')[1] + inputfile_dict[key] = '"%s"' % param.split('=', 1)[1].strip() else: # if it is completly new then directly write to new input file - value = param.split('=')[1] + value = param.split('=', 1)[1].strip() INPUTFILE.set(new_cfg, key, "\"%s\"" % value) # append the remaining input file entries to the new input file @@ -197,12 +173,20 @@ def create_config_file(interface_details, config_type): logger.debug("ignoring hnv address as there is no cfg for %s", virtual['adapter_type']) continue - return create_config_inputs(orig_cfg, new_cfg, inputfile, virtual, config_type=config_type) + return create_config_inputs(orig_cfg, new_cfg, inputfile, virtual, config_type=config_type) + + # If we reach here, interface_details was empty or all items were skipped + logger.warning("No valid virtual interface config found in create_config_file") + return None def create_config(interface_details, config_type): """ Creates avocado test suite / config file, and input file needed for yaml files in that config files. """ + test_suites = [] + input_file_string = "" + input_params = [] + if config_type == 'pci': for pci in interface_details: if pci['is_root_disk']: @@ -231,10 +215,19 @@ def create_config(interface_details, config_type): logger.debug("ignoring pci address %s as there is no cfg for %s", pci['pci_root'], pci['adapter_type']) continue - test_suites, input_file_string, input_params = create_config_inputs(orig_cfg, new_cfg, inputfile, pci, config_type='pci') + + result = create_config_inputs(orig_cfg, new_cfg, inputfile, pci, config_type='pci') + if result is None: + logger.warning("No input params found for %s; skipping input file generation", orig_cfg) + continue + test_suites, input_file_string, input_params = result if config_type in ('vnic', 'veth', 'hnv'): - test_suites, input_file_string, input_params = create_config_file(interface_details, config_type) + result = create_config_file(interface_details, config_type) + if result is None: + logger.warning("No input params found for virtual interface; skipping input file generation") + return "" + test_suites, input_file_string, input_params = result test_suites = ",".join(test_suites) @@ -281,18 +274,173 @@ def create_config(interface_details, config_type): help='Run the test suite using created test config and input files') parser.add_argument('--additional-params', dest='add_params', action='store', default='', - help='Additional parameters(key=value) to the input file, space separated') + help='Additional parameters(key=value) to the input file, comma separated') + parser.add_argument('--params-file', dest='params_file', + action='store', default=None, + help='Path to a file containing dynamic key=value parameters ' + '(one per line) to inject into the input file at runtime. ' + 'Useful for Jenkins CR runs where IO adapters, LPAR names, ' + 'IPs etc. change per build without editing the base input file. ' + 'Lines starting with # are treated as comments and ignored. ' + 'Values in --params-file are merged with --additional-params; ' + '--additional-params takes precedence on duplicate keys. ' + 'Example file content:\n' + ' interface=eth0\n' + ' lpar=lpar1\n' + ' wwpn=0x500507680d1e4c00') + parser.add_argument('--remote-server', dest='remote_server', + action='store', default=None, + help='Hostname or IP of the remote machine to gather interface details from') + parser.add_argument('--remote-user', dest='remote_user', + action='store', default='root', + help='SSH username for the remote machine (used with --remote-server, default: root)') + parser.add_argument('--remote-password', dest='remote_password', + action='store', default=None, + help='SSH password for the remote machine (used with --remote-server)') + parser.add_argument('--hmc-ip', dest='hmc_ip', + action='store', default=None, + help='HMC hostname or IP address to query for managed system name') + parser.add_argument('--hmc-user', dest='hmc_user', + action='store', default='hscroot', + help='HMC SSH username (default: hscroot)') + parser.add_argument('--hmc-password', dest='hmc_password', + action='store', default=None, + help='HMC SSH password (required with --hmc-ip)') args = parser.parse_args() - #if no interfaces name is provided for vNIC ,vETH or HNV, get the first aavailable interface. + if args.params_file: + if not os.path.isfile(args.params_file): + logger.error("Params file not found: %s", args.params_file) + sys.exit(1) + # Connection/credential keys are read from the file and injected + # directly into args (if not already set via CLI). They are NOT + # forwarded into add_params so they never end up in the mux input file. + _CREDENTIAL_KEYS = { + 'peer_public_ip': 'remote_server', + 'hmc_ip': 'hmc_ip', + } + # These keys set an args attribute AND are forwarded to the input file. + _CREDENTIAL_KEYS_ALSO_FORWARD = { + 'peer_password': 'remote_password', + 'hmc_pwd': 'hmc_password', + 'vios_pwd': 'vios_pwd', + } + file_params = [] + with open(args.params_file, 'r') as pf: + for line in pf: + line = line.strip() + if not line or line.startswith('#'): + continue + if '=' not in line: + logger.warning("Skipping invalid line in params file (no '='): %s", line) + continue + key = line.split('=')[0].strip() + value = line.split('=', 1)[1].strip() + if key in _CREDENTIAL_KEYS: + attr = _CREDENTIAL_KEYS[key] + # CLI value always wins; only set from file if not already provided + if not getattr(args, attr, None): + setattr(args, attr, value) + logger.debug("%s loaded from params file", key) + else: + logger.debug("%s already set via CLI, ignoring params file value", key) + elif key in _CREDENTIAL_KEYS_ALSO_FORWARD: + attr = _CREDENTIAL_KEYS_ALSO_FORWARD[key] + # Set args attribute (credential use) + if not getattr(args, attr, None): + setattr(args, attr, value) + logger.debug("%s loaded from params file (credential)", key) + # Also forward to input file + file_params.append(line) + else: + file_params.append(line) + if file_params: + # Build a set of keys already in --additional-params so they take precedence + cli_keys = set() + if args.add_params: + for p in args.add_params.split(','): + p = p.strip() + if '=' in p: + cli_keys.add(p.split('=')[0].strip()) + # Append only file params whose keys are NOT already in --additional-params + merged = [p for p in file_params if p.split('=')[0].strip() not in cli_keys] + if args.add_params: + args.add_params = args.add_params.rstrip(',') + ',' + ','.join(merged) + else: + args.add_params = ','.join(merged) + logger.info("Loaded %d param(s) from params file: %s", len(merged), args.params_file) + + # Validate remote args: server + user + password must all be available + # (each can come from CLI or from --params-file: peer_public_ip / peer_password) + if args.remote_server and not args.remote_user: + parser.error("--remote-server requires --remote-user " + "(provide via CLI or add 'peer_username=' to --params-file)") + if args.remote_server and not args.remote_password: + parser.error("--remote-server requires --remote-password " + "(provide via CLI or add 'peer_password=' to --params-file)") + + # Auto-detect HMC IP from lsrsrc IBM.MCP if not supplied + if not args.hmc_ip: + detected_ip = get_hmc_ip_from_lsrsrc() + if detected_ip: + logger.info("Auto-detected HMC IP: %s", detected_ip) + args.hmc_ip = detected_ip + + # Auto-fetch HMC password from secrets manager if not supplied + if args.hmc_ip and not args.hmc_password: + fetched_pwd = get_hmc_password_from_secrets_manager() + if fetched_pwd: + logger.info("HMC password retrieved from secrets manager") + args.hmc_password = fetched_pwd + else: + logger.warning( + "Could not auto-fetch HMC password. " + "Provide it manually via --hmc-password to enable manageSystem lookup." + ) + + # Build an optional RemoteRunner when --remote-server is supplied. + # The runner is passed down to virtual.* functions so every shell + # command executes on the remote host over SSH instead of locally. + _runner = None + if args.remote_server: + logger.info("Remote mode: connecting to %s as %s", args.remote_server, args.remote_user) + _runner = RemoteRunner( + host=args.remote_server, + username=args.remote_user, + password=args.remote_password, + ) + + # Local interface discovery always runs on the local machine (no runner). + # The runner is used exclusively for gathering peer_* remote details below. if args.vnic_int == 'vnic_default': - args.vnic_int = virtual.get_vnic_interface_names()[0] + vnic_ifaces = virtual.get_vnic_interface_names() + if vnic_ifaces: + args.vnic_int = vnic_ifaces[0] + else: + logger.error("No vNIC interfaces found") + sys.exit(1) if args.veth_int == 'veth_default': - args.veth_int = virtual.get_veth_interface_names()[0] + veth_ifaces = virtual.get_veth_interface_names() + if veth_ifaces: + args.veth_int = veth_ifaces[0] + else: + logger.error("No vETH interfaces found") + sys.exit(1) if args.hnv_int == 'hnv_default': - args.hnv_int = virtual.get_hnv_interface_names()[0] + hnv_ifaces = virtual.get_hnv_interface_names() + if hnv_ifaces: + args.hnv_int = hnv_ifaces[0] + else: + logger.error("No HNV interfaces found") + sys.exit(1) + + # Initialize all interface detail variables to satisfy type checker + vnic_details = [] + veth_details = [] + hnv_details = [] + pci_details = [] try: if args.vnic_int: @@ -310,8 +458,159 @@ def create_config(interface_details, config_type): logger.info("vNIC interface not found") else: logger.info("No PCI Found") + if _runner: + _runner.close() sys.exit(0) + # ------------------------------------------------------------------ # + # manageSystem: query HMC for the managed system name of the local LPAR. + # Requires --hmc-ip and --hmc-password. Uses lparstat to get LPAR name, + # then searches all managed systems on the HMC for a match. + # ------------------------------------------------------------------ # + _manage_system = '' + if args.hmc_ip: + try: + import subprocess as _sp + _lpar_name_out = _sp.getoutput( + "lparstat -i 2>/dev/null | grep 'Partition Name' | awk -F': ' '{print $2}'" + ).strip() + if _lpar_name_out: + logger.info("Local LPAR name detected: %s", _lpar_name_out) + with HMCClient(hmc_ip=args.hmc_ip, username=args.hmc_user, + password=args.hmc_password) as hmc: + _manage_system = hmc.get_managed_system_for_lpar(_lpar_name_out) or '' + logger.info("Managed system resolved: %s", _manage_system) + else: + logger.warning("Could not determine local LPAR name via lparstat; skipping manageSystem lookup") + except Exception as _hmc_err: + logger.warning("HMC lookup failed: %s", _hmc_err) + + # Inject manageSystem + VIOS info into all virtual interface detail dicts + _vios_names = '' + _vios_ip = '' + if args.hmc_ip and args.hmc_password and _manage_system: + try: + with HMCClient(hmc_ip=args.hmc_ip, username=args.hmc_user, + password=args.hmc_password) as hmc: + _vios_info = hmc.get_vios_info(_manage_system) + _vios_names = _vios_info.get('vios_names', '') + _vios_ip = _vios_info.get('vios_ip', '') + logger.info("VIOS names: %s VIOS IPs: %s", _vios_names, _vios_ip) + except Exception as _vios_err: + logger.warning("Could not retrieve VIOS info: %s", _vios_err) + + for _details in [ + vnic_details if args.vnic_int else [], + veth_details if args.veth_int else [], + hnv_details if args.hnv_int else [], + ]: + for _d in _details: + _d['manageSystem'] = _manage_system + _d['vios_names'] = _vios_names + _d['vios_ip'] = _vios_ip + + # ------------------------------------------------------------------ # + # host_ip: derive 192.168.10. from the local public IP. + # Applied for vnic/veth/hnv regardless of whether --remote-server is set. + # ------------------------------------------------------------------ # + for _details in [ + vnic_details if args.vnic_int else [], + veth_details if args.veth_int else [], + hnv_details if args.hnv_int else [], + ]: + for _d in _details: + pub_ip = _d.get('public_interface_ip', '') or '' + if pub_ip: + last_octet = pub_ip.split('.')[-1] + _d['host_ip'] = '192.168.10.%s' % last_octet + + # Assign host_ips based on number of interfaces + num_interfaces = len(_d.get('interfaces', [])) + if num_interfaces > 1: + _d['host_ips'] = '192.168.10.%s 192.168.20.%s' % (last_octet, last_octet) + _d['netmasks'] = '255.255.255.0 255.255.255.0' + else: + _d['host_ips'] = '192.168.10.%s' % last_octet + _d['netmasks'] = '255.255.255.0' + + logger.info( + "Derived host_ip=%s host_ips=%s from public_interface_ip=%s (interfaces: %d)", + _d['host_ip'], _d['host_ips'], pub_ip, num_interfaces, + ) + else: + _d['host_ip'] = '' + _d['host_ips'] = '' + logger.debug("public_interface_ip not available; host_ip/host_ips left empty") + _d['netmask'] = '255.255.255.0' + + # ------------------------------------------------------------------ # + # Peer enrichment: when --remote-server is given, gather peer_* fields + # from the remote machine and inject them into the local vnic/veth/hnv + # details dict so pci_input.conf mappings (peer_ip, peer_ips, + # peer_interfaces, peer_public_ip) are resolved automatically. + # ------------------------------------------------------------------ # + if _runner and args.vnic_int: + try: + logger.info("Gathering peer vNIC details from remote host %s", args.remote_server) + peer_ifaces = virtual.get_vnic_interface_names(runner=_runner) or [] + # peer_interfaces: first two vNIC interface names (space-separated) + peer_interfaces_val = " ".join(peer_ifaces[:2]) + # peer_ip: IP of the first remote vNIC interface + peer_ip_val = "" + if peer_ifaces: + peer_ip_val = virtual.get_interface_ip(peer_ifaces[0], runner=_runner) or "" + # peer_ips: IPs of the first two remote vNIC interfaces (space-separated) + peer_ips_list = [] + for iface in peer_ifaces[:2]: + ip = virtual.get_interface_ip(iface, runner=_runner) + if ip: + peer_ips_list.append(ip) + peer_ips_val = " ".join(peer_ips_list) + # peer_public_ip: public IP of the remote host (net0) + peer_public_ip_val = virtual.get_host_public_ip(runner=_runner) or "" + + # Inject into the first (and only) dict in vnic_details + vnic_details[0]['peer_ip'] = peer_ip_val + vnic_details[0]['peer_ips'] = peer_ips_val + vnic_details[0]['peer_interfaces'] = peer_interfaces_val + vnic_details[0]['peer_public_ip'] = peer_public_ip_val + logger.info( + "Peer info injected: peer_ip=%s peer_ips=%s peer_interfaces=%s peer_public_ip=%s", + peer_ip_val, peer_ips_val, peer_interfaces_val, peer_public_ip_val, + ) + except Exception as peer_err: + logger.warning("Could not gather peer details from %s: %s", args.remote_server, peer_err) + + if _runner and args.veth_int: + try: + logger.info("Gathering peer vETH details from remote host %s", args.remote_server) + peer_ifaces = virtual.get_veth_interface_names(runner=_runner) or [] + peer_interfaces_val = " ".join(peer_ifaces[:2]) + peer_ip_val = virtual.get_interface_ip(peer_ifaces[0], runner=_runner) if peer_ifaces else "" + peer_ips_val = " ".join(filter(None, [virtual.get_interface_ip(i, runner=_runner) for i in peer_ifaces[:2]])) + peer_public_ip_val = virtual.get_host_public_ip(runner=_runner) or "" + veth_details[0]['peer_ip'] = peer_ip_val or "" + veth_details[0]['peer_ips'] = peer_ips_val + veth_details[0]['peer_interfaces'] = peer_interfaces_val + veth_details[0]['peer_public_ip'] = peer_public_ip_val + except Exception as peer_err: + logger.warning("Could not gather peer details from %s: %s", args.remote_server, peer_err) + + if _runner and args.hnv_int: + try: + logger.info("Gathering peer HNV details from remote host %s", args.remote_server) + peer_ifaces = virtual.get_hnv_interface_names(runner=_runner) or [] + peer_interfaces_val = " ".join(peer_ifaces[:2]) + peer_ip_val = virtual.get_interface_ip(peer_ifaces[0], runner=_runner) if peer_ifaces else "" + peer_ips_val = " ".join(filter(None, [virtual.get_interface_ip(i, runner=_runner) for i in peer_ifaces[:2]])) + peer_public_ip_val = virtual.get_host_public_ip(runner=_runner) or "" + hnv_details[0]['peer_ip'] = peer_ip_val or "" + hnv_details[0]['peer_ips'] = peer_ips_val + hnv_details[0]['peer_interfaces'] = peer_interfaces_val + hnv_details[0]['peer_public_ip'] = peer_public_ip_val + except Exception as peer_err: + logger.warning("Could not gather peer details from %s: %s", args.remote_server, peer_err) + if args.show_info: if args.pci_addr: pprint(pci_details) @@ -322,6 +621,7 @@ def create_config(interface_details, config_type): elif args.hnv_int: pprint(hnv_details) + cmd = "" if args.create_cfg: if args.vnic_int: cmd = create_config(interface_details=vnic_details, config_type='vnic') @@ -338,3 +638,7 @@ def create_config(interface_details, config_type): if args.run_test: os.system(cmd) + + # Always close the SSH connection when done + if _runner: + _runner.close()