# Copyright (c) 2024-2025 Broadcom. All Rights Reserved. # The term "Broadcom" refers to Broadcom Inc. # and/or its subsidiaries. import ipaddress import glob import logging import os import pathlib import re import shutil import socket import sys import xml.etree.ElementTree as ET from enum import Enum from lib.console import print_text_warning from lib.constants import RHTTPPROXY_CONFIG_FILE_PATH, STS_SERVER_CONFIG_FILE_PATH, STS_SERVER_CONFIG_PROPERTY_FILE_PATH from lib.environment import Environment from lib.execution_replay import ReplayContext from lib.command_runner import CommandRunner from lib.java_utils import load_property_file from lib.text_utils import TextFilter VMAFD_CLI = '/usr/lib/vmware-vmafd/bin/vmafd-cli' LWREGSHELL_CLI = '/opt/likewise/bin/lwregshell' TEE_CMD = '/usr/bin/tee' FILE_CMD = '/usr/bin/file' CHMOD_CMD = '/bin/chmod' logger = logging.getLogger(__name__) class VcVersion(Enum): """ Define the recognized versions of VC. """ # We assume the needed version values are only major and minor releases, # and not patches for example. This allows us to represent the versions as # floating point numbers for relative comparison. For code specific to # patch releases, the tool resorts to build numbers, which are checked as # needed. # # Also, the order of enum definitions must be kept in increasing order, # including minor releases. Invalid = None V7 = '7' V8 = '8' V9 = '9' @classmethod def min(cls): """Minimum supported vCenter version""" return min([float(v) for v in cls if v.value is not None]) @classmethod def max(cls): """Maximum supported vCenter version""" return max([float(v) for v in cls if v.value is not None]) @property def major(self): """Return the major release number for the release.""" if self.value is None: return None # Floats that are actually integers always end with ".0" as a string. return str(float(self)).split('.')[0] @property def minor(self): """Return the minor release number for the release.""" if self.value is None: return None # Floats that are actually integers always end with ".0" as a string. return str(float(self)).split('.')[1] def _validate(self, other): if not isinstance(other, VcVersion): raise ValueError(other) def __float__(self): # If there is no value (for the Invalid entry) then return float NaN # so all comparison operations return False. (However, we provide # special direct comparison overrides for != and == so that comparisons # with VcVersion.Invalid work as expected.) return float('nan') if self.value is None else float(self.value) def __lt__(self, other): self._validate(other) return float(self) < float(other) def __le__(self, other): self._validate(other) return float(self) <= float(other) def __eq__(self, other): self._validate(other) return self.value == other.value def __ne__(self, other): self._validate(other) return self.value != other.value def __gt__(self, other): self._validate(other) return float(self) > float(other) def __ge__(self, other): self._validate(other) return float(self) >= float(other) def init_env_host(): """ Obtain basic information from VC server """ vpxd_info = CommandRunner('vpxd', '-v').run_and_get_output().strip().split(' ') vc_version_long = vpxd_info[2] vc_version = '.'.join(vc_version_long.split('.')[:-1]) vc_build = vpxd_info[3].split('-')[1] hostname = CommandRunner('hostname', '-f').run_and_get_output().strip() pnid = CommandRunner(VMAFD_CLI, 'get-pnid', '--server-name', 'localhost').run_and_get_output().strip() machine_id = CommandRunner(VMAFD_CLI, 'get-machine-id', '--server-name', 'localhost').run_and_get_output().strip() ifconfig_output = CommandRunner('/usr/sbin/ifconfig', 'eth0').run_and_get_output() ip_address = TextFilter(ifconfig_output).head(2).tail(1).get_text().strip().split(' ')[0].replace(':', ' ').split(' ')[-1] sso_domain = CommandRunner(VMAFD_CLI, 'get-domain-name', '--server-name', 'localhost').run_and_get_output().strip() sso_site = CommandRunner(VMAFD_CLI, 'get-site-name', '--server-name', 'localhost').run_and_get_output().strip() sso_domain_dn = "dc={}".format(",dc=".join(sso_domain.split('.'))) vmdir_account_dn, vmdir_account_password = get_vmdir_machine_account() local_hostname = socket.getfqdn() env = Environment.get_environment() env.set_value('VC_VERSION', vc_version) env.set_value('VC_VERSION_LONG', vc_version_long) env.set_value('VC_BUILD', vc_build) env.set_value('HOSTNAME', hostname) env.set_value('LOCAL_HOSTNAME', local_hostname) env.set_value('PNID', pnid) env.set_value('MACHINE_ID', machine_id) env.set_value('IP_ADDRESS', ip_address) env.set_value('SSO_DOMAIN', sso_domain) env.set_value('SSO_SITE', sso_site) env.set_value('SSO_DOMAIN_DN', sso_domain_dn) env.set_value('VMDIR_MACHINE_ACCOUNT_DN', vmdir_account_dn) env.set_value('VMDIR_MACHINE_ACCOUNT_PASSWORD', vmdir_account_password) smart_card_filter_file = get_smart_card_filter_file() env.set_value('SMART_CARD_FILTER_FILE', smart_card_filter_file) init_env_solution_users() def get_vmdir_machine_account(): """ Get machine account from likewise :return: a tupple of ( account_dn, account_password) """ output = CommandRunner(LWREGSHELL_CLI, 'list_values', r'[HKEY_THIS_MACHINE\services\vmdir]').run_and_get_output() account_dn = TextFilter(output).contain('"dcAccountDN"').cut('REG_SZ', [1]).get_text().strip()[1:-1]\ .replace('\\"', r'"').replace('\\\\', '\\') account_password = TextFilter(output).contain('dcAccountPassword').cut('REG_SZ', [1]).get_text().strip()[1:-1]\ .replace('\\"', r'"').replace('\\\\', '\\') return account_dn, account_password def is_file_exists(file): """ Wrapper for checking file existence. This wrapper is required to redirect operation when VCERT_REMOTE_EXEC is True :param file: file path to be checked :return: True if file is exist """ if not is_remote_exec(): return os.path.exists(file) return_code, _, _ = CommandRunner('/bin/ls', file, expected_return_code=None).run() return return_code == 0 def get_config_file_path(config_file): env = Environment.get_environment() if os.path.isabs(config_file): if is_file_exists(config_file): return config_file else: print_text_warning("Unable to find configuration file '{}'".format(config_file)) print_text_warning('Script cannot continue. Exiting...') sys.exit(1) else: vcert_relative_config_path = "{}/{}".format(env.get_value('SCRIPT_DIR'), config_file) cwd_relative_config_path = "{}/{}".format(os.getcwd(), config_file) if is_file_exists(vcert_relative_config_path): return vcert_relative_config_path elif is_file_exists(cwd_relative_config_path): return cwd_relative_config_path else: print_text_warning("Configuration file does not exist in the vCert script directory ({}),".format(vcert_relative_config_path)) print_text_warning("nor in the current working directory ({}).".format(cwd_relative_config_path)) print_text_warning('Script cannot continue. Exiting...') sys.exit(1) def make_directory(path): """ Wrapper for making directory. This wrapper is required to redirect operation when VCERT_REMOTE_EXEC is True :param path: directory path to be created :return: True if the operation success, otherwise False """ if is_remote_exec(): command_runner = CommandRunner('/bin/mkdir', '-p', path, expected_return_code=None) return_code, _, _ = command_runner.run() return return_code == 0 try: pathlib.Path(path).mkdir(parents=True, exist_ok=True) return True except Exception: return False def remove_directory(path, remove_all=False): """ Wrapper for removing directory This wrapper is required to redirect operation when VCERT_REMOTE_EXEC is True :param path: Directory path to remove :param remove_all: if True, this method will remove the directory contents before trying to remove the directory :return: True if the operation success, otherwise False """ if is_remote_exec(): if remove_all: command_runner = CommandRunner('/bin/rmdir', '-r', path, expected_return_code=None) else: command_runner = CommandRunner('/bin/rmdir', path, expected_return_code=None) return_code, _, _ = command_runner.run() return return_code == 0 try: if remove_all: shutil.rmtree(path) else: pathlib.Path(path).rmdir() except Exception: return False def get_file_contents(file): """ Wrapper for reading file contents. This wrapper is required to redirect operation when VCERT_REMOTE_EXEC is True :param file: File path to load :return: File contents """ if not is_remote_exec(): with open(file, 'r') as f: return f.read() command_runner = CommandRunner('/bin/cat', file, expected_return_code=0) _, stdout, _ = command_runner.run() return stdout def get_hostname(): """ Wrapper obtaining remote hostname This wrapper is required to redirect operation when VCERT_REMOTE_EXEC is True :return: return VCERT_REMOTE_HOSTNAME value if VCERT_REMOTE_EXEC is True, otherwise return 'localhost' """ if is_remote_exec(): hostname = Environment.get_environment().get_value('VCERT_REMOTE_HOSTNAME') else: hostname = 'localhost' return hostname def is_remote_exec(): """ Check if remote execution is set :return: True if remote execution is set, otherwise False """ return Environment.get_environment().get_value('VCERT_REMOTE_EXEC') is True def get_vc_version(): """ Get the VC version. The value is obtained from environment variable VC_VERSION :return: VC version as VcVersion enum """ vc_version = Environment.get_environment().get_value('VC_VERSION') # Versions in VcVersion are kept in increasing order. Thus we search the # versions in reverse order to bulletproof the code against versions that # are represented just as a major release versus ones that are represented # as major.minor. for definedVcVersion in reversed(VcVersion): if ( definedVcVersion.value is not None and vc_version.startswith(definedVcVersion.value)): return definedVcVersion return VcVersion.Invalid def init_env_solution_users(): """ Initialize set of solution users to be checked, saved the list into environment variable 'SOLUTION_USERS' """ solutionUserDB = { # Solution User Version Constraint (None means all) # ------------------- ----------------------------------- 'machine': None, 'vsphere-webclient': None, 'vpxd': None, 'vpxd-extension': None, 'hvc': None, 'wcp': [VcVersion.V7, VcVersion.V8], 'wcpsvc': [VcVersion.V9], } env = Environment.get_environment() vc_version = get_vc_version() solution_users = [] for solutionUser, versionConstraint in solutionUserDB.items(): if versionConstraint and not vc_version in versionConstraint: continue solution_users.append(solutionUser) env.set_value('SOLUTION_USERS', solution_users) def is_valid_ip_address(hostname_or_ip): try: ipaddress.ip_address(hostname_or_ip) return True except ValueError: return False def get_ip_address(hostname_or_ip): if is_valid_ip_address(hostname_or_ip): return hostname_or_ip else: return socket_gethostbyname(hostname_or_ip) def socket_gethostbyname(hostname): category = 'other' command_args = ['socket', 'gethostbyname', 'hostname', hostname] context = ReplayContext.get_replay_context() if context and context.is_replaying: result = context.get_execution_result(category, command_args, None) if result is not None: _, ip_address, _ = result return ip_address try: ip_address = socket.gethostbyname(hostname) except socket.gaierror: ip_address = '' if context and context.is_capturing: context.store_result(category, command_args, None, 0, ip_address, None) return ip_address def save_text_to_file(text, filename): """ A wrapper to write {text} to file {filename} :param filename: the output filename :param text: file contents """ if is_remote_exec(): CommandRunner(TEE_CMD, filename, command_input=text, expected_return_code=0).run() return with open(filename, 'w') as file: file.write(text) def append_text_to_file(text, filename, start_new_line=True): """ A wrapper to append {text} to file {filename} :param text: text to append :param file: the existing filename :param start_new_line: start appending data on a new line """ if is_remote_exec(): CommandRunner(TEE_CMD, '-a', filename, command_input=text, expected_return_code=0).run() return with open(filename, 'a') as file: if start_new_line: file.write("\n{}".format(text)) else: file.write(text) def find_files(pattern): """ Wrapper for glob.glob({pattern}) method :param pattern: glob patter for matching """ if is_remote_exec(): output = CommandRunner('/usr/bin/sh', '-c', "eval ls -1 '{}'".format(pattern)).run_and_get_output() return output.splitlines() return glob.glob(pattern) def get_file_type(filename): """ Get file type returned by /usr/bin/file command """ output = CommandRunner(FILE_CMD, filename, expected_return_code =0).run_and_get_output() return TextFilter(output).cut(delimiter=':', fields=[1]).get_text().strip() def set_file_mode(file_path, mode): """ A wrapper for setting file mode on {filename} :param file_path: File path to be modified :param mode: permission value, the exact permission value to be passed to os.chmod() """ if is_remote_exec(): mode_string = "{:04o}".format(mode) CommandRunner(CHMOD_CMD, mode_string, file_path, expected_return_code=0).run() else: os.chmod(file_path, mode) def get_smart_card_filter_file(): def revproxy_get_file(): # Before 7.0 U3i the location of the Smart Card filter file is defined # in the reverse proxy config. config = ET.parse(RHTTPPROXY_CONFIG_FILE_PATH) root = config.getroot() client_ca_list_file = root.find('clientCAListFile') if client_ca_list_file is None: filter_file = '' else: filter_file = client_ca_list_file.text filter_file = '/etc/vmware-rhttpproxy/{}'.format(filter_file) if filter_file.startswith('/') is False else filter_file return filter_file def xml_config_get_file(): # After 7.0 U3i the location of the Smart Card filter file is # hard-coded in the STS server config which is an XML file. config = ET.parse(STS_SERVER_CONFIG_FILE_PATH) root = config.getroot() for connector in root.iter('Connector'): if connector.attrib['port'] == '${bio-ssl-clientauth.https.port}': return connector.find('SSLHostConfig').attrib['truststoreFile'] return '' # Not found def prop_config_get_file(): # Starting with 9.0 the Smart Card filter file is in the STS server # configuration file which is a Java property file. config = load_property_file(STS_SERVER_CONFIG_PROPERTY_FILE_PATH) return config.get('vmidentity.server.connector.ssl.client.auth.truststore.file', '') # In the following DB, search stops on first match. # Order of entries in the DB must be from most specific to least specific # (for example an entry with a version specifying a build must be before # the same version with no build). smartCardConfigDB = [ # Version Build Retrieval Function # ------------- -------- --------------------- (VcVersion.V7, 20845200, xml_config_get_file), (VcVersion.V7, None, revproxy_get_file), (VcVersion.V8, None, xml_config_get_file), (VcVersion.V9, None, prop_config_get_file), ] env = Environment.get_environment() vc_version = get_vc_version() vc_build = int(env.get_value('VC_BUILD')) # Resolve the retrieval function. We break on first match. get_file = None for versionConstraint, buildConstraint, func in smartCardConfigDB: if vc_version != versionConstraint: continue if buildConstraint and vc_build < buildConstraint: continue # Found a match. get_file = func break # If found, execute the retrieval function; otherwise return an empty # string. return get_file() if get_file else ''