add vmware-tools

This commit is contained in:
Your Name
2026-06-23 11:27:54 +08:00
parent 2a794fa8d3
commit 4f39d384e2
198 changed files with 14170 additions and 0 deletions
View File
@@ -0,0 +1,688 @@
# Copyright (c) 2024-2025 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import base64
import subprocess
import OpenSSL
import re
from datetime import datetime
from lib.exceptions import CommandExecutionError
from lib.command_runner import CommandRunner
from lib.console import print_text_error
from lib.input import MenuInput
from lib.host_utils import get_file_contents, get_file_type
from lib.text_utils import TextFilter
OPENSSL_CLI = '/usr/bin/openssl'
CERTOOL_CLI = '/usr/lib/vmware-vmca/bin/certool'
def get_command_output_with_input(args, input_text):
"""
Get command output with the specified input
:param args: Command and its arguments
:param input_text: input text to be supplied to stdin
:return: command output from stdout
"""
cmd = CommandRunner(*args, command_input=input_text, expected_return_code=0)
_, stdout, _ = cmd.run()
return stdout
def get_certificate_info(pem_text):
"""
Get certificate information from openssl command
This is a wrapper for 'openssl x509 -text -fingerprint -noout' command
:param pem_text: Certificate in PEM format
:return: Certificate info generated by openssl command
"""
args = [OPENSSL_CLI, 'x509', '-noout', '-text', '-fingerprint']
return get_command_output_with_input(args, pem_text).strip()
def get_subject_hash(pem_cert):
"""
Get the Subject hash for a certificate
This is a wrapper for the 'openssl x509 -noout -subject_hash' command
:param pem_cert: Base64 certificate hash
:return: Subject hash returned by openssl
"""
args = [OPENSSL_CLI, 'x509', '-noout', '-subject_hash']
return get_command_output_with_input(args, pem_cert).strip()
def get_formatted_dn(x509_name, use_slash_sep=False):
"""
Get formatted distinguished name for subject and issuer
The original vCert depends on 'openssl x509 -subject' format, but the output format
varies between openssl versions.
- openssl 1.0 (VC 6.x):
subject= /CN=CA/DC=vsphere/DC=local/C=US/ST=California/O=sc2-10-184-104-251.eng.vmware.com/OU=VMware Engineering
- openssl 3.0 (VC 7.x, 8.x):
subject=CN = CA, DC = vsphere, DC = local, C = US, ST = California, O = sc2-10-184-104-251.eng.vmware.com, OU = VMware Engineering
- openssl 3.2:
subject=CN=CA, DC=vsphere, DC=local, C=US, ST=California, O=sc2-10-184-104-251.eng.vmware.com, OU=VMware Engineering
Unless requested, we will use the last one since it's the most widely used format.
:param x509_name: OpenSSL.crypto.X509Name object
:param use_slash_sep: Use slash separator
:return: formatted distinguished name
"""
comps = []
comp_map = dict()
for comp in x509_name.get_components():
comp_name = str(comp[0], 'utf-8')
comp_value = str(comp[1], 'utf-8')
comp_map[comp_name] = comp_value
comps.append("{}={}".format(comp_name, comp_value))
if use_slash_sep:
return "/{}".format('/'.join(comps))
else:
return ', '.join(comps)
def get_subject_and_issuer_dn(x509_cert):
"""
Get Subject DN and Issuer DN from X509 certificate in a formatted DN
:param x509_cert: X509Certificate object
:return: tuple (formatted subject DN, formatted issuer DN)
"""
return get_formatted_dn(x509_cert.get_subject()), get_formatted_dn(x509_cert.get_issuer())
def get_certificate_extensions(x509_cert):
"""
Get X509 certificate extensions as a dict object
:param x509_cert: X509 certificate
:return: X509 certificate extensions
"""
extensions = dict()
for idx in range(x509_cert.get_extension_count()):
ext = x509_cert.get_extension(idx)
ext_short_name = ext.get_short_name().decode('utf-8')
try:
extensions[ext_short_name] = str(ext)
except OpenSSL.crypto.Error:
pass
return extensions
def get_subject_keyid(x509_cert, remove_colons=False, allow_alternate=False):
"""
Get Subject Key Identifier from X509v3 extension
If the keyid is not available and {allow_alternate} is True, the keyid
will be generated using serial/subject_dn pair.
:param x509_cert: X509 certificate
:param remove_colons: Remove colon separators
:param allow_alternate: Allow alternate keyid value using serial/subject_dn
:return: subject keyid
"""
extensions = get_certificate_extensions(x509_cert)
skid = extensions.get('subjectKeyIdentifier')
if skid:
if remove_colons:
skid = skid.replace(':', '')
elif allow_alternate:
serial = get_serial_number(x509_cert)
subject_dn = get_formatted_dn(x509_cert.get_subject(), use_slash_sep=True)
skid = "DirName:{},serial:{}".format(subject_dn, serial)
else:
skid = ''
return skid
def get_authority_keyid(x509_cert, remove_colons=False, allow_alternate=False):
"""
Get Authority Key Identifier from X509v3 extension
If the keyid is not available and {allow_alternate} is True, the keyid
will be generated using serial/DirName pair, if available
:param x509_cert: X509 certificate
:param remove_colons: Remove colon separators
:param allow_alternate: Allow alternate keyid value using serial/subject_dn
:return: authority keyid
"""
akid = ''
extensions = get_certificate_extensions(x509_cert)
akids = extensions.get('authorityKeyIdentifier')
if akids:
akid = TextFilter(akids).start_with('keyid:').get_text()
if not akid and len(akids.splitlines()) == 1 and len(akids.split(':')) >= 7:
akid = akids
if akid:
akid = akid.replace('keyid:', '')
if remove_colons:
akid = akid.replace(':', '')
elif akids and allow_alternate:
dir_name = TextFilter(akids).start_with('DirName:').get_text()
serial = TextFilter(akids).start_with('serial:').get_text()
if dir_name and serial:
akid = "{},{}".format(dir_name, serial)
return akid
def get_serial_number(x509_cert):
"""
Get certificate serial number in colon separated capitalized text,
e.g. 88:EC:FC:3B:FB:3F:53:52
:param x509_cert: X509 certificate
:return: certificate serial number
"""
serial = "{:X}".format(x509_cert.get_serial_number())
if len(serial) % 2 != 0:
serial = "0{}".format(serial)
parts = [serial[idx:idx+2] for idx in range(0, len(serial), 2)]
return ":".join(parts)
def get_certificate_start_date(x509_cert):
"""
Get certificate start date
:param x509_cert: X509 certificate object
:return: Certificate's start date as a datetime object
"""
input_format = '%Y%m%d%H%M%SZ'
return datetime.strptime(x509_cert.get_notBefore().decode('utf-8'), input_format)
def get_certificate_end_date(x509_cert):
"""
Get certificate end date
:param x509_cert: X509 certificate object
:return: Certificate's end date as a datetime object
"""
input_format = '%Y%m%d%H%M%SZ'
return datetime.strptime(x509_cert.get_notAfter().decode('utf-8'), input_format)
def get_certificate_fingerprint(x509_cert, algorithm=None, remove_colons=False):
"""
Get certificate fingerprint
:param x509_cert: X509 certificate object
:param algorithm: fingerprint algorithm. If it's not specified, use sha1
:param remove_colons: remove colon delimiters
:return:
"""
if algorithm is None:
algorithm = 'sha1'
fingerprint = x509_cert.digest(algorithm).decode('utf-8')
return fingerprint.replace(':', '') if remove_colons else fingerprint
def get_x509_certificate(pem_cert):
"""
Load X509Certificate from PEM text
:param pem_cert: Certificate in PEM format
:return: X509Certificate object
"""
return OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pem_cert)
def get_certificate_expiry_in_days(x509_cert):
"""
Return certificate expiry in days
:param x509_cert: X509 certificate object
:return: certificate's expiry in days
"""
return (get_certificate_end_date(x509_cert) - datetime.utcnow()).days
def is_x509_expired(x509_cert):
"""
Checks if x509 object is expired
"""
days_left = get_certificate_expiry_in_days(x509_cert)
return True if days_left < 0 else False
def is_cert_expired(pem_cert):
"""
Checks if Base64 certificate is expired
"""
x509_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pem_cert)
return is_x509_expired(x509_cert)
def is_cert_file_expired(cert_file):
"""
Checks if PEM certificate file is expired
"""
pem_cert = get_file_contents(cert_file)
return is_cert_expired(pem_cert)
def get_subject_alternative_names(x509_cert):
"""
Get SAN entries in the certificate
:param x509_cert: X590 certificate object
:return: list of SAN entries (including the prefixes)
"""
extensions = get_certificate_extensions(x509_cert)
return extensions.get('subjectAltName')
def get_cert_info_for_path_building(pem_cert):
"""
Obtain the basic certificate information for certificate path
building. The information includes:
- subject keyId
- authority keyId
- certificate name (common name)
- whether the certificate is self-signed
- whether the certificate is trusted
- whether the certificate is a CA
- pathlen of the certificate basic constraints
(or False if basic constraints are not present)
:param pem_cert: Certificate in PEM format
:return: dict object contains the required information
"""
x509_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pem_cert)
subject_dn, issuer_dn = get_subject_and_issuer_dn(x509_cert)
skid = get_subject_keyid(x509_cert, remove_colons=True, allow_alternate=True)
akid = get_authority_keyid(x509_cert, remove_colons=True, allow_alternate=True)
basic_constraints = get_certificate_extensions(x509_cert).get('basicConstraints')
if basic_constraints is not None:
pathlen_search = re.search('pathlen:[0-9]+', basic_constraints)
pathlen = pathlen_search.group().split(':')[1] if pathlen_search is not None else False
else:
pathlen = False
if skid and akid:
is_selfsigned = skid == akid
else:
is_selfsigned = subject_dn == issuer_dn
cert_name = get_certificate_name(pem_cert)
return {
"subject_keyid": skid,
"auth_keyid": akid,
"cert_name": cert_name,
"is_selfsigned": is_selfsigned,
"is_trusted": False,
"is_ca": is_ca_certificate(x509_cert),
"pathlen": pathlen,
}
def get_certificate_fetcher_from_list(pem_certs):
"""
Method to compose certificate fetcher closure method from certificate list
in PEM text. The fetcher method will be used by build_certification_path
method.
:param pem_certs: List of certificate in PEM format
:return: subject keyId list, fetcher method
"""
subject_keyids = []
cert_map = dict()
for pem_cert in pem_certs:
cert = get_x509_certificate(pem_cert)
subject_keyid = get_subject_keyid(cert, remove_colons=True, allow_alternate=True)
if subject_keyid:
cert_map[subject_keyid] = pem_cert
subject_keyids.append(subject_keyid)
def fetch_certificate_closure(skid):
return cert_map.get(skid)
return subject_keyids, fetch_certificate_closure
def build_certification_path(pem_cert, trusted_subject_keyids, certificate_fetcher):
"""
Build certification path using a strict method of subject keyId and authority keyId
matching.
:param pem_cert: Certificate in PEM format. It may contain intermediate CA certificate
:param trusted_subject_keyids: list of subject keyId of trusted root CA certificates
:param certificate_fetcher: method to fetch certificate from subject keyId
:return: list of certificate info, from leaf certificate to root CA certificate
"""
cert_map = dict()
machine_certs = split_certificates_from_pem(pem_cert)
cur_cert = None
for idx, machine_cert in enumerate(machine_certs):
cert_info = get_cert_info_for_path_building(machine_cert)
subject_keyid = cert_info['subject_keyid']
cert_info['is_trusted'] = subject_keyid in trusted_subject_keyids
if idx == 0:
cur_cert = cert_info
else:
cert_map[subject_keyid] = cert_info
cert_path = [cur_cert]
while not cur_cert['is_selfsigned'] and cur_cert['auth_keyid'] is not None:
auth_keyid = cur_cert['auth_keyid']
cert_info = cert_map.get(auth_keyid)
if not cert_info:
next_pem_cert = certificate_fetcher(auth_keyid)
if not next_pem_cert:
break
cert_info = get_cert_info_for_path_building(next_pem_cert)
if cert_info['subject_keyid'] != auth_keyid:
break
cert_info['is_trusted'] = True
cert_map[cert_info["subject_keyid"]] = cert_info
cert_path.insert(0, cert_info)
cur_cert = cert_info
return cert_path
def split_certificates_from_pem(pem):
"""
Get list of individual certificates from PEM text
:param pem: Certificates in PEM format
:return: list of certificates in PEM format
"""
return TextFilter(pem).match_block('-----BEGIN CERTIFICATE-----',
'-----END CERTIFICATE-----', concatenate=True).get_lines()
def get_certificate_from_host(hostname, port=443):
"""
Get server TLS certificate from host via SSL handshake
:param hostname: hostname to connect
:param port: port
:return: Server certificate in PEM format
"""
remote_host = "{}:{}".format(hostname, port)
output = CommandRunner(OPENSSL_CLI, 's_client', '-connect', remote_host, '-showcerts',
command_input=subprocess.DEVNULL).run_and_get_output()
pem_cert = TextFilter(output).match_block('^-----BEGIN CERTIFICATE-----$',
'^-----END CERTIFICATE-----$',
concatenate=True).get_text()
return pem_cert
def build_pem_certificate(cert_text):
"""
Build PEM certificate based on certificate text input
This is required since certificates are often stored in DB
using a single line text.
:param cert_text: certificate text to be processed
:return: return certificate in a proper PEM format
"""
if type(cert_text) is bytes:
if cert_text.startswith(b'-----BEGIN CERTIFICATE-----'):
pem_cert = cert_text.replace(b'\\n', b'\n').decode('utf-8').strip()
elif cert_text.startswith(b'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t'):
pem_cert = base64.decodebytes(cert_text).decode('utf-8').strip()
else:
pem_cert = '\n'.join(['-----BEGIN CERTIFICATE-----',
base64.encodebytes(cert_text).decode('utf-8').strip(),
'-----END CERTIFICATE-----'])
else:
if cert_text.startswith('-----BEGIN CERTIFICATE-----'):
pem_cert = cert_text.replace('\\n', '\n')
else:
pem_cert = '\n'.join(['-----BEGIN CERTIFICATE-----', cert_text.strip(),
'-----END CERTIFICATE-----'])
# reformat to ensure that each line less than or equal to 72 characters
lines = pem_cert.splitlines()
contents = ''.join(lines[1:-1])
result = [lines[0]]
for line in contents.splitlines():
while len(line) > 72:
result.append(line[:72])
line = line[72:]
if line:
result.append(line)
result.append(lines[-1])
return '\n'.join(result)
def is_ca_certificate(x509_cert):
"""
Check if certificate is a CA certificate
:param x509_cert: certificate
:return: True of a CA certificate, otherwise False
"""
basic_constraints = get_certificate_extensions(x509_cert).get('basicConstraints')
key_usage = get_certificate_extensions(x509_cert).get('keyUsage')
is_ca = basic_constraints is not None and 'CA:TRUE' in basic_constraints
can_sign_certs = key_usage is not None and 'Certificate Sign' in key_usage
return is_ca and can_sign_certs
def is_self_signed_certificate(x509_cert):
"""
Check if the certificate is a self-signed certificate
:param x509_cert: X509 certificate
:return: True if self-signed, otherwise False
"""
subject_kid = get_subject_keyid(x509_cert, allow_alternate=True)
auth_keyid = get_authority_keyid(x509_cert, allow_alternate=True)
if subject_kid and auth_keyid:
return subject_kid == auth_keyid
else:
subject_dn, issuer_dn = get_subject_and_issuer_dn(x509_cert)
return subject_dn == issuer_dn
def generate_vmca_signed_certificate(config, output_dir, filename):
"""
Generate VMCA signed certificate using certool utility
In the {output_dir} directory, the following will be created:
- {filename}.key : the private key
- {filename}.pub : the public key
- {filename}.crt : the certificate file
:param config: certool config file
:param output_dir: The output directory
:param filename: The filename for the output (without extension)
"""
privkey_arg = "--privkey={}/{}.key".format(output_dir, filename)
pubkey_arg = "--pubkey={}/{}.pub".format(output_dir, filename)
CommandRunner(CERTOOL_CLI, '--genkey', privkey_arg, pubkey_arg, expected_return_code=0).run()
config_arg = "--config={}".format(config)
cert_arg = "--cert={}/{}.crt".format(output_dir, filename)
CommandRunner(CERTOOL_CLI, privkey_arg, '--gencert', cert_arg, config_arg, expected_return_code=0).run()
def detect_and_convert_to_pem(input_cert, password=None, allow_input=False):
"""
Detect certificate file format and convert it to PEM certificate as necessary
:param input_cert: The certificate file
:param password: In case of PKCS#12, the store password
:param allow_input: In case of PKCS#12, allow the method to ask for password
:return: tuple of ( certificate in PEM, private key in PEM of None )
"""
file_type = get_file_type(input_cert)
if 'PEM' in file_type or 'ASCII' in file_type:
contents = get_file_contents(input_cert)
cert_pem, key_pem = get_certificate_and_private_key(contents)
if cert_pem or key_pem:
return cert_pem, key_pem
# need conversion, try PKCS#12 format
if is_pkcs12_file(input_cert):
result = convert_pkcs12_to_pem(input_cert)
if password is not None:
result = convert_pkcs12_to_pem(input_cert, password)
if result is None:
result = convert_pkcs12_to_pem(input_cert, 'Antidisestablishmentarianism123581321',
allow_input=allow_input)
if result is None:
raise CommandExecutionError('Unable to validate keystore password')
return result
# DER format
cert_pem = CommandRunner(OPENSSL_CLI, 'x509', '-inform', 'der', '-in', input_cert,
'-outform', 'pem').run_and_get_output().strip()
key_pem = CommandRunner(OPENSSL_CLI, 'rsa', '-inform', 'der', '-in', input_cert,
'-outform', 'pem').run_and_get_output().strip()
output = "{}\n{}".format(cert_pem, key_pem) if cert_pem or key_pem else ''
if not output:
# PKCS#7, PEM
output = CommandRunner(OPENSSL_CLI, 'pkcs7', '-print_certs', '-inform', 'pem', '-in',
input_cert, '-outform', 'pem').run_and_get_output().strip()
if not output:
# PKCS#7, DER
output = CommandRunner(OPENSSL_CLI, 'pkcs7', '-print_certs', '-inform', 'der', '-in',
input_cert, '-outform', 'pem').run_and_get_output().strip()
if output:
return get_certificate_and_private_key(output)
else:
raise CommandExecutionError('Unsupported file format')
def is_pkcs12_file(filename):
ret_code, _, stderr = CommandRunner(OPENSSL_CLI, 'pkcs12', '-noout', '-in', filename,
'-passin', 'pass:').run()
if ret_code != 0 and 'asn1 error' in stderr:
return False
else:
return True
def convert_pkcs12_to_pem(cert_file, password=None, allow_input=False):
"""
Convert PKCS#12 format to PEM
:param cert_file:
:param password:
:param allow_input:
:return:
"""
password_arg = "pass:{}".format(password if password is not None else '')
ret_code, stdout, stderr = CommandRunner(OPENSSL_CLI, 'pkcs12', '-nokeys', '-info',
'-in', cert_file, '-passin', password_arg, '-nomacver').run()
if ret_code != 0 and allow_input and 'pkcs12 cipherfinal error' in stderr:
retry = 0
password = ''
while retry < 3 and not password:
password = MenuInput('Enter password for PKCS#12 keystore: ',
case_insensitive=False, masked=True).get_input()
password_arg = "pass:{}".format(password)
ret_code, stdout, _ = CommandRunner(OPENSSL_CLI, 'pkcs12', '-nokeys', '-info',
'-in', cert_file, '-passin', password_arg, '-nomacver').run()
if ret_code != 0:
print_text_error('Invalid password.')
password = ''
retry += 1
if ret_code == 0:
cert_pem, _ = get_certificate_and_private_key(stdout)
ret_code, stdout, _ = CommandRunner(OPENSSL_CLI, 'pkcs12', '-nocerts', '-nodes',
'-info', '-in', cert_file, '-passin', password_arg, '-nomacver').run()
_, key_pem = get_certificate_and_private_key(stdout)
return cert_pem, key_pem
return None
def get_certificate_and_private_key(text):
"""
Separate certificate and private key parts from PEM text
:param text: PEM text contains certificate and private key
"""
cert_pem = TextFilter(text)\
.match_block('^-----BEGIN CERTIFICATE-----',
'^-----END CERTIFICATE-----').get_text()
key_pem = TextFilter(text) \
.match_block('^-----BEGIN .*PRIVATE KEY-----',
'^-----END .*PRIVATE KEY-----') \
.get_text()
return cert_pem, key_pem
def get_key_modulus_from_pem_text(cert_or_key_text):
"""
Get the key modulus from certificate or private key text (PEM)
:param cert_or_key_text: certificate or key file
:return: The key modulus
"""
if TextFilter(cert_or_key_text).match('^-----BEGIN CERTIFICATE-----').get_lines():
modulus = CommandRunner(OPENSSL_CLI, 'x509', '-noout', '-modulus',
command_input=cert_or_key_text).run_and_get_output()
elif TextFilter(cert_or_key_text).match('^-----BEGIN .*PRIVATE KEY-----').get_text():
modulus = CommandRunner(OPENSSL_CLI, 'rsa', '-noout', '-modulus',
command_input=cert_or_key_text).run_and_get_output()
else:
return None
return TextFilter(modulus).cut('=', [1]).get_text().strip()
def generate_csr(openssl_config, csr_file, key_file):
"""
Generate CSR file using config {openssl_config}
:param openssl_config: OpenSSL config file
:param csr_file: The output CSR file
:param key_file: The output key file
"""
command = CommandRunner(OPENSSL_CLI, 'req', '-new', '-newkey', 'rsa:3072', '-nodes', '-config',
openssl_config, '-out', csr_file, '-keyout', key_file, expected_return_code=0)
command.run()
def load_pem_certificate_file_in_der(cert_file, leaf_only=False):
"""
Load PEM certificates and return them in DER format
"""
certs_pem = split_certificates_from_pem(get_file_contents(cert_file))
certs_der = []
for cert_pem in certs_pem:
cert_x509 = get_x509_certificate(cert_pem)
certs_der.append(OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, cert_x509))
if leaf_only:
return certs_der[:1]
return certs_der
def load_pem_key_file_in_pkcs8_der(key_file):
"""
Load PEM key file and return them in PKCS#8 DER format
"""
key_der = CommandRunner(OPENSSL_CLI, 'pkcs8', '-topk8', '-inform', 'pem', '-outform',
'der', '-in', key_file, '-out', '/dev/stdout', '-nocrypt',
command_input=subprocess.DEVNULL, binary_output=True).run_and_get_output()
return key_der
def get_certificate_name(pem_cert, target_dn='subject') -> str:
"""
Get the name of the entity in an x509 certificate, taken from the
Subject attributes CommonName, OrgUnit, and Organization (in that order of preference)
:param pem_cert: Base64 hash of a certificate
:return: string of the computed certificate name
"""
x509_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pem_cert)
# get certificate name, typically will return the common name
comps = dict()
dn = x509_cert.get_subject() if target_dn == 'subject' else x509_cert.get_issuer()
for comp in dn.get_components():
comp_name = str(comp[0], 'utf-8')
comp_value = str(comp[1], 'utf-8')
comps[comp_name] = comp_value
cert_name = '<unknown>'
for key in ['CN', 'OU', 'O']:
if comps.get(key):
cert_name = comps[key]
break
return cert_name
+178
View File
@@ -0,0 +1,178 @@
# Copyright (c) 2024 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import subprocess
from lib.environment import Environment
from lib.exceptions import CommandExecutionError, CommandExecutionTimeout
from lib.execution_replay import ReplayContext
class CommandRunner(object):
"""
A utility class to execute external command and obtain the output
This class also provide a mechanism for rerouting the execution on
remote machine via SSH session, capturing the execution result and
replaying it back for development and testing purpose.
"""
def __init__(self, *command_args, **options):
"""
CommandRunner constructor
:param command_args: the command and its arguments
:param options: Supports the following optional parameters:
- expected_return_code: if specified, CommandRunner will check the command
return value and will raise CommandExecutionError it doesn't match
- command_input: Text to be supplied to stdin when executing the command
- binary_output: indicating that the command is expected to return binary output
"""
self.command_args = command_args
self.options = options
self.timeout = None
self.replay_context = None
if options.get('expected_return_code') is not None:
self.expected_return_code = options['expected_return_code']
else:
self.expected_return_code = None
if options.get('command_input') is not None:
self.command_input = options['command_input']
else:
self.command_input = None
if options.get('binary_output') is not None:
self.binary_output = options['binary_output'] is True
else:
self.binary_output = False
if options.get('timeout') is not None:
self.timeout = options['timeout']
self.remote_hostname = None
self.remote_username = None
self.is_remote = False
self.setup_remote_exec()
def setup_remote_exec(self):
"""
Setup the remote execution, capture/replay mechanism if the required
keys are set in the environment
"""
env = Environment.get_environment()
if env.get_value('VCERT_REMOTE_EXEC'):
self.set_remote(env.get_value('VCERT_REMOTE_HOSTNAME'),
env.get_value('VCERT_REMOTE_USERNAME'))
if env.get_value('VCERT_REMOTE_EXEC_REPLAY') is True \
or env.get_value('VCERT_REMOTE_EXEC_CAPTURE') is True:
self.replay_context = ReplayContext.get_replay_context()
def set_remote(self, hostname, username='root'):
self.is_remote = True
self.remote_hostname = hostname
self.remote_username = username if not username else 'root'
def set_input(self, command_input):
self.command_input = command_input
def run(self):
"""
Run the command, redirect to run_remote if remote execution is set
"""
if self.is_remote:
return self.run_remote(self.command_args, self.command_input, self.timeout,
self.expected_return_code, self.binary_output)
else:
return self.run_local(self.command_args, self.command_input, self.timeout,
self.expected_return_code, self.binary_output)
def run_and_get_output(self):
"""
Run the command and get the standard output only
"""
_, stdout, _ = self.run()
return stdout
@staticmethod
def run_local(command_args, command_input, timeout, expected_return_code, binary_output) -> (int, str, str):
"""
Run the command locally
:param command_args: external command and the arguments
:param command_input: Text to be supplied as stdin
:param timeout: Timeout value for waiting the external command to
return. It will raise CommandExecutionTimeout when this happen
:param expected_return_code: The expected return code. If it's
specified and it doesn't match to the actual return code,
CommandExecutionError will be raised
:param binary_output: need to handle binary output instead of text
:return: a tuple of (return code, stdout output, stderr output)
"""
try:
if binary_output:
ret = subprocess.run(command_args, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, timeout=timeout)
elif command_input == subprocess.DEVNULL:
ret = subprocess.run(command_args, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, universal_newlines=True, timeout=timeout)
else:
ret = subprocess.run(command_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
input=command_input, universal_newlines=True, timeout=timeout)
if expected_return_code is not None:
if ret.returncode != expected_return_code:
raise CommandExecutionError("External command '{}' returned {}, error message: {}".format(
" ".join(command_args), ret.returncode, ret.stderr))
return ret.returncode, ret.stdout, ret.stderr
except subprocess.TimeoutExpired:
raise CommandExecutionTimeout("External command '{}' timed out".format(" ".join(command_args)))
def run_remote(self, command_args, command_input, timeout, expected_return_code, binary_output):
"""
Run the command on remote machine, or perform capture/replay when
set in the environment variables
It will append the required ssh command arguments 'ssh', '-l', '<user>', '<hostname>'
The capture and replay mechanism will use the remote setting for storing
and retrieve the command result.
Refer to run_local for the parameter description
"""
env = Environment.get_environment()
remote_hostname = env.get_value('VCERT_REMOTE_HOSTNAME')
remote_username = env.get_value('VCERT_REMOTE_USERNAME')
local_hostname = env.get_value('LOCAL_HOSTNAME')
if self.replay_context and self.replay_context.is_replaying:
result = self.replay_context.get_execution_result('command', command_args, command_input)
if result is not None:
if expected_return_code is not None:
return_code, _, _ = result
if return_code != expected_return_code:
raise CommandExecutionError("External command '{}' returned {}".format(
" ".join(command_args), return_code))
return result
if local_hostname and remote_hostname.lower() == local_hostname.lower():
# run locally
final_args = command_args
else:
final_args = ['ssh', '-l', remote_username, remote_hostname]
final_args.extend(command_args)
CommandRunner.add_quotation_escape(final_args)
return_code, stdout, stderr = CommandRunner.run_local(final_args, command_input, timeout,
None, binary_output)
if self.replay_context and self.replay_context.is_capturing:
self.replay_context.store_result('command', command_args, command_input, return_code, stdout, stderr)
if expected_return_code is not None and expected_return_code != return_code:
raise CommandExecutionError("External command '{}' returned {}".format(
" ".join(command_args), return_code))
return return_code, stdout, stderr
@staticmethod
def add_quotation_escape(args):
for index, arg in enumerate(args):
if ' ' in arg or '"' in arg or '\'' in arg or '\\' in arg:
args[index] = "\"{}\"".format(arg.replace('"', '\\"'))
+155
View File
@@ -0,0 +1,155 @@
# Copyright (c) 2024 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import sys
import tempfile
from lib.environment import Environment, DefaultDict
from lib.text_utils import translate_text
class ColorKey(object):
"""
Class for holding color mapping to its terminal escape sequence
"""
RED = '{COLORS[RED]}'
GREEN = '{COLORS[GREEN]}'
BLUE = '{COLORS[BLUE]}'
YELLOW = '{COLORS[YELLOW]}'
CYAN = '{COLORS[CYAN]}'
LIGHT_BLUE = '{COLORS[LIGHT_BLUE]}'
UNDER_LINE = '{COLORS[UNDER_LINE]}'
NORMAL = '{COLORS[NORMAL]}'
def init_env_colormap():
"""
Initialize color map in the environment
"""
env = Environment.get_environment()
color_disabled = env.get_value('COLORS_DISABLED') is True
if color_disabled:
color_map = DefaultDict()
else:
color_map = {
'RED': '\033[31m',
'GREEN': '\033[32m',
'YELLOW': '\033[33m',
'BLUE': '\033[34m',
'CYAN': '\033[36m',
'LIGHT_BLUE': '\033[94m',
'UNDER_LINE': '\033[04m',
'NORMAL': '\033[00m'
}
env.set_value('COLORS', color_map)
def set_text_color(color):
"""
Set the color. The color will affect any normal print() calls following this method call
:param color: ColorKey object
"""
print_text(color, end='')
def print_text(*text, max_width=0, sep='', end='\n'):
"""
Print text with additional preprocessing based
:param text: Text to be print. The text will be translated first
:param max_width: if > 0, new line will be added automatically if each line
:param sep: separator between text
:param end: add end text after the last of line
"""
text = translate_text(*text, sep=sep)
if max_width > 0:
text_len = len(text)
idx = 0
while idx < text_len:
if idx + max_width < text_len:
print(text[idx:max_width])
else:
print(text[idx:], end=end)
else:
print(text, end=end)
sys.stdout.flush()
def print_text_warning(text, end='\n'):
"""
Print warning text with yellow color.
:param text: Text to be print
:param end: end characters at the end of text
"""
print_text(ColorKey.YELLOW, text, ColorKey.NORMAL, sep='', end=end)
def print_text_error(text, end='\n'):
"""
Print error text. By default it's identical to print_text_warning,
printing text with yellow color.
"""
print_text_warning(text, end=end)
def print_header(text):
"""
Print header text
"""
print()
print_text("{}{}".format(ColorKey.CYAN, text))
print_text("{}{}".format('-' * 65, ColorKey.NORMAL))
def print_task(text):
"""
Print formatted text for task name
"""
print("{:<52}".format(text), end='', flush=True)
def print_task_status(status_text, color=ColorKey.GREEN):
"""
Print formatted text for task status
"""
set_text_color(color)
print("{:>13}".format(status_text), end='')
set_text_color(ColorKey.NORMAL)
print(flush=True)
def print_task_status_warning(result):
"""
Print warning task status (color: yellow)
"""
print_task_status(result, ColorKey.YELLOW)
def print_task_status_error(result):
"""
Print error task status (color: red)
"""
print_task_status(result, ColorKey.RED)
def capture_method_output(method, *args, **kwargs):
"""
Invoke the method and capture the stdout output produced by this method
:param method: method to be invoked
:return: captured stdout output
"""
stdout = sys.stdout
try:
with tempfile.NamedTemporaryFile('w+') as file:
sys.stdout = file
method(*args, **kwargs)
file.seek(0)
return file.read()
finally:
sys.stdout = stdout
init_env_colormap()
+42
View File
@@ -0,0 +1,42 @@
# Copyright (c) 2024-2026 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
VCERT_PROGRAM = 'vCert.py'
VCERT_NAME = 'VCF/VVF Certificate Management Utility'
VCERT_VERSION = '6.1.1'
VCERT_DESC = "{} (version {})".format(VCERT_NAME, VCERT_VERSION)
TOP_DIR = '/storage/vCert'
LOG_DIR = '/var/log/vmware/vCert'
REPORT_FILE_PATH = "{}/vcenter-certificate-report.txt".format(LOG_DIR)
VMCA_CERT_FILE_PATH = '/var/lib/vmware/vmca/root.cer'
VMCA_KEY_FILE_PATH = '/var/lib/vmware/vmca/privatekey.pem'
VMCA_SSO_FILE_PATH = '/etc/vmware-sso/keys/ssoserverRoot.crt'
VMCAM_CERT_FILE_PATH = '/var/lib/vmware/vmcam/ssl/vmcamcert.pem'
RBD_CERT_FILE_PATH = '/etc/vmware-rbd/ssl/rbd-ca.crt'
AUTH_PROXY_CERT_FILE_PATH = '/var/lib/vmware/vmcam/ssl/rui.crt'
RHTTPPROXY_CONFIG_FILE_PATH = '/etc/vmware-rhttpproxy/config.xml'
# For VC prior to version 9.0 the STS server config file was an XML file.
# Beginning with version 9.0 the format was converted to a Java property file.
STS_SERVER_CONFIG_FILE_PATH = '/usr/lib/vmware-sso/vmware-sts/conf/server.xml'
STS_SERVER_CONFIG_PROPERTY_FILE_PATH = '/usr/lib/vmware-sso/vmware-sts/conf/catalina.properties'
LOCALHOST = 'localhost'
READ = 'read'
WRITE = 'write'
SPACE = " "
WARNING_TEXT = """
{COLORS[YELLOW]}------------------------!!! Attention !!!------------------------{COLORS[NORMAL]}
This script is intended to be used at the direction of Broadcom Global Support.
Changes made could render this system inoperable. Please ensure you have a valid
VAMI-based backup or offline snapshots of {COLORS[UNDER_LINE]}{COLORS[YELLOW]}ALL{COLORS[NORMAL]} vCenter/PSC nodes in the SSO domain
before continuing. Please refer to the following Knowledge Base article:
{COLORS[UNDER_LINE]}{COLORS[YELLOW]}https://knowledge.broadcom.com/external/article?legacyId=85662{COLORS[NORMAL]}
Do you acknowledge the risks and wish to continue? [y/n]: """
VMWARE_DEPOT_HOST = 'dl.broadcom.com'
VMWARE_DEPOT_CERT_ISSUER = 'DigiCert TLS RSA SHA256 2020 CA1'
+82
View File
@@ -0,0 +1,82 @@
# Copyright (c) 2024 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import yaml
from pathlib import Path
class DefaultDict(dict):
def __missing__(self, key):
return ''
class Environment(object):
shared_environment = None
def __init__(self):
self.cache = DefaultDict()
self.load_default()
self.restricted_keys = dict()
def get_value(self, key):
value = self.cache.get(key)
if value is None and key in self.restricted_keys.keys():
for k, v in self.restricted_keys[key]():
self.cache[k] = v
if k == key:
value = v
return value
def set_value(self, key, value):
if key in self.restricted_keys.keys():
raise KeyError('Cannot update environment using restricted key')
self.cache[key] = value
def invalidate_value(self, key):
self.cache[key] = None
def get_map(self):
return self.cache
def add_restricted_key(self, key, populate_method):
"""
Set {key} as restricted variable. The existing value is invalidated.
:param key: environment key
:param populate_method: method to be called to populate the values
"""
if key in self.restricted_keys:
raise KeyError('Duplicating restricted key')
self.restricted_keys[key] = populate_method
self.cache[key] = None
def load_from_file(self, env_file):
"""
Load environment variables from config file
:param env_file: config file
"""
self.cache = DefaultDict()
self.restricted_keys = dict()
with open(env_file, 'r') as file:
config = yaml.safe_load(file)
env_map = config['environments']
for key in env_map.keys():
self.set_value(key, env_map[key])
def load_default(self):
"""
Load environment variables from default config file 'config/env.yaml'
"""
base_dir = str(Path(__file__).resolve().parent.parent)
default_env_file = Path(base_dir, 'config/env.yaml')
if Path.exists(default_env_file):
self.load_from_file(str(default_env_file))
@staticmethod
def get_environment():
if Environment.shared_environment is None:
Environment.shared_environment = Environment()
return Environment.shared_environment
+39
View File
@@ -0,0 +1,39 @@
# Copyright (c) 2024 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
class BaseScriptException(Exception):
def __init__(self, error_msg):
super().__init__(error_msg)
class LdapException(BaseScriptException):
"""
Base Exception class for all the Exception in ldap functionality
"""
def __init__(self, error_code, description):
self.error_msg = "LDAP exception error code {} ({})".format(error_code, description)
super().__init__(self.error_msg)
def __str__(self):
return self.error_msg
class MenuExitException(BaseScriptException):
pass
class CommandExecutionError(BaseScriptException):
pass
class CommandExecutionTimeout(BaseScriptException):
pass
class ReplayEntryNotFound(BaseScriptException):
pass
class OperationFailed(BaseScriptException):
pass
@@ -0,0 +1,240 @@
# Copyright (c) 2024 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import base64
import json
import glob
import os
import subprocess
from collections.abc import Mapping
from datetime import datetime
from lib.environment import Environment
from lib.exceptions import ReplayEntryNotFound, CommandExecutionError
replay_context = None
class ReplayEntry(object):
"""
Class for representing execution result entry
"""
def __init__(self, category, command_args, input_string, tag):
"""
ReplayEntry constructor
:param category: category of result (e.g. 'command', 'ldap')
:param command_args: command and the parameters
:param input_string: Input text to be supplied as stdin
"""
self.category = category
self.command_args = command_args
self.input_string = input_string if input_string is not None else ''
self.used_count = 0
self.key = self.generate_key(category, command_args, input_string, tag)
self.results = []
def add_result(self, timestamp, return_code, stdout, stderr):
"""
Add command result entry
:param timestamp: the timestamp of command execution
:param return_code: command's return code
:param stdout: The command output from stdout
:param stderr: The command output from stderr
"""
# always add result in sorted
added = False
value = (timestamp, return_code, stdout, stderr)
for index, (ts, _, _, _) in enumerate(self.results):
if ts > timestamp:
self.results.insert(index, value)
added = True
break
if not added:
self.results.append(value)
@staticmethod
def generate_key(category, command_args, input_string, tag):
"""
Generate key based on category, command_args, and input_string.
The key is not guaranteed to be unique but should be good enough for
debugging and testing purpose. This key will be used to retrieve
the previous command result for replay
:param category: the category, 'command', 'ldap'
:param command_args: command arguments
:param input_string: input string
:param tag: additional tag to differentiate the same commands in a different condition
:return: generated key
"""
for idx, arg in enumerate(command_args):
if not isinstance(arg, str):
command_args[idx] = str(arg)
if input_string == subprocess.DEVNULL:
input_string = '__/dev/null__'
return "{}||{}||{}||{}".format(category, ">>".join(command_args), input_string, tag)
class ReplayContext(object):
"""
The class holding context object for capturing or replaying command result
"""
categories = ['command', 'ldap', 'other']
def __init__(self, replay_dir, is_replaying=True, is_capturing=False):
"""
ReplayContext constructor
:param replay_dir: base directory for storing or retrieving command result
:param is_capturing: True when capturing, otherwise it's in replay mode
"""
self.replay_dir = replay_dir
self.replay_entries = None
self.is_replaying = is_replaying
self.is_capturing = is_capturing
self.tag = None
def get_replay_entry(self, key) -> ReplayEntry:
"""
Get replay entry based on the key
:param key: key to be used to retrieve the command result
"""
if self.replay_entries is None:
self.load_all_replays()
return self.replay_entries.get(key)
def store_result(self, category, command_args, command_input, return_code, stdout, stderr):
"""
Store the command result
The file will be stored in <replay-dir>/<hostname>/replay_<category>_<1st command_args>_<timestamp>.json
"""
command = command_args[0].split('/')[-1]
timestamp = datetime.utcnow().strftime('%Y%m%d%H%M%S_%f')
file_name = "{}/replay_{}_{}_{}.json".format(self.replay_dir, category,
command, timestamp)
with open(file_name, 'w') as file:
content = {
"command_args": ReplayContext.encode_bytes(command_args),
"input": command_input if command_input != subprocess.DEVNULL else '__/dev/null__',
"tag": self.tag,
"timestamp": timestamp,
"return_code": return_code,
"stdout": self.encode_bytes(stdout),
"stderr": self.encode_bytes(stderr)
}
try:
file.write(json.dumps(content, indent=4))
except TypeError as te:
raise CommandExecutionError("Unserializable object: {}".format(content))
if self.is_replaying:
self.add_replay_entry(category, command_args, command_input, self.tag, timestamp,
return_code, stdout, stderr)
def set_tag(self, tag):
self.tag = tag
def load_all_replays(self):
"""
Load all replay entries
"""
self.replay_entries = {}
for category in ReplayContext.categories:
for file in sorted(glob.glob("{}/replay_{}_*.json".format(self.replay_dir, category))):
with open(file, 'r') as f:
replay = json.load(f)
command_args = ReplayContext.decode_bytes(replay['command_args'])
command_input = replay['input']
stdout = ReplayContext.decode_bytes(replay['stdout'])
stderr = ReplayContext.decode_bytes(replay['stderr'])
self.add_replay_entry(category, command_args, command_input, replay['tag'], replay['timestamp'],
replay['return_code'], stdout, stderr)
def add_replay_entry(self, category, command_args, command_input, tag, timestamp,
return_code, stdout, stderr):
"""
Add replay entry
"""
key = ReplayEntry.generate_key(category, command_args, command_input, tag)
entry = self.get_replay_entry(key)
if entry is None:
entry = ReplayEntry(category, command_args, command_input, tag)
self.replay_entries[key] = entry
entry.add_result(timestamp, return_code, stdout, stderr)
def reset_context(self):
for entry in self.replay_entries:
entry.used_count = 0
def get_execution_result(self, category, command_args, command_input) -> (int, str, str):
"""
Get the previous execution result based on {category}, {command_args}, and {command_input}
"""
key = ReplayEntry.generate_key(category, command_args, command_input, self.tag)
entry = self.get_replay_entry(key)
if entry is None:
if self.is_capturing:
return None
else:
raise ReplayEntryNotFound("Replay entry not found for key {}".format(key))
result = entry.results[entry.used_count]
entry.used_count = (entry.used_count + 1) % len(entry.results)
return result[1:]
@staticmethod
def get_replay_context(reload=False):
"""
Static method to get ReplayContext instance from other modules
:param reload: if True, it will dispose the previous object and create a new instance
"""
global replay_context
env = Environment.get_environment()
is_remote = env.get_value('VCERT_REMOTE_EXEC') is True
is_replay = env.get_value('VCERT_REMOTE_EXEC_REPLAY') is True
is_capture = env.get_value('VCERT_REMOTE_EXEC_CAPTURE') is True
if reload:
replay_context = None
if is_remote and (is_replay or is_capture) and replay_context is None:
remote_hostname = env.get_value('VCERT_REMOTE_HOSTNAME')
replay_dir = "{}/{}".format(env.get_value('VCERT_REMOTE_EXEC_REPLAY_DIR'),
remote_hostname)
if not os.path.exists(replay_dir):
os.makedirs(replay_dir)
replay_context = ReplayContext(replay_dir, is_replay, is_capture)
return replay_context
@staticmethod
def encode_bytes(obj):
if isinstance(obj, bytes):
return "__base64__({})".format(base64.b64encode(obj).decode('utf-8'))
if isinstance(obj, list):
for idx, value in enumerate(obj):
obj[idx] = ReplayContext.encode_bytes(value)
elif isinstance(obj, dict):
for key in obj.keys():
obj[key] = ReplayContext.encode_bytes(obj[key])
elif isinstance(obj, Mapping):
new_obj = dict()
for key in obj.keys():
new_obj[key] = ReplayContext.encode_bytes(obj[key])
obj = new_obj
return obj
@staticmethod
def decode_bytes(obj):
if isinstance(obj, str) and obj.startswith('__base64__(') and obj.endswith(')'):
return base64.b64decode(obj[11:-1])
if isinstance(obj, list):
for idx, value in enumerate(obj):
obj[idx] = ReplayContext.decode_bytes(value)
elif isinstance(obj, dict):
for key in obj.keys():
obj[key] = ReplayContext.decode_bytes(obj[key])
return obj
+502
View File
@@ -0,0 +1,502 @@
# 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 ''
+97
View File
@@ -0,0 +1,97 @@
# Copyright (c) 2024 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import getpass
from lib.console import ColorKey, print_text
from lib.environment import Environment
from lib.text_utils import translate_text
class MenuInput(object):
"""
Class for getting user input. This class is separated from the rest of menu
related classes due to a circular module dependency.
"""
def __init__(self, text, acceptable_inputs=None, default_input=None, allow_empty_input=False,
case_insensitive=True, masked=False):
"""
MenuInput constructor
:param text: Text to be displayed on console before obtaining the user input
:param acceptable_inputs: All acceptable input user can enter
:param default_input: The default input to be shown and to be used when the user entered an empty input
:param allow_empty_input: If this parameter is True, the input can return empty string when
no default_input was specified
:param case_insensitive: Ignore text case. If this option is True, MenuInput will return input with
upper case
:param masked: If this is True, user typed characters will not be shown on the console (e.g. for password
input
"""
self.text = text
if acceptable_inputs and case_insensitive:
self.acceptable_inputs = [s.upper() for s in acceptable_inputs]
else:
self.acceptable_inputs = acceptable_inputs
self.default_input = default_input
self.allow_empty_input = allow_empty_input
self.case_insensitive = case_insensitive
self.masked = masked
def get_acceptable_input_str(self) -> str:
"""
Return acceptable input is a simplified text
This method expects that the acceptable_inputs will be in the following order:
1, 2, ..., <num>, X, Y
For above case, this method will return string like '1-<num>, X, Y'
:return: Returns acceptable input text
"""
keys = [s for s in self.acceptable_inputs if not s.isnumeric()]
numeric_keys_count = len(self.acceptable_inputs) - len(keys)
if numeric_keys_count == 1:
keys.insert(0, '1')
elif numeric_keys_count > 1:
keys.insert(0, "{}-{}".format(1, numeric_keys_count))
return ", ".join(keys)
@staticmethod
def builtin_input_wrapper(text, masked):
"""
A wrapper method for the builtin method 'input'. This method is used to simplify
mocking user input for the unit testing
"""
if masked:
return getpass.getpass(text)
return input(text)
def get_input(self) -> str:
"""
Executing MenuInput: show the text, obtain and validate the user input
"""
if self.default_input is not None:
env = Environment.get_environment()
env.set_value('CURRENT_MENU', {'__DEFAULT__': self.default_input})
while True:
text = translate_text(self.text)
input_text = self.builtin_input_wrapper(text, self.masked)
if self.case_insensitive:
input_text = input_text.upper()
if not input_text:
if self.default_input is not None:
return self.default_input
if self.allow_empty_input:
return ''
else:
print_text('{}Invalid input.{}'.format(ColorKey.YELLOW, ColorKey.NORMAL))
continue
elif self.acceptable_inputs:
if input_text in self.acceptable_inputs:
return input_text
else:
print_text("\n{}Invalid input. The acceptable inputs: {}{}\n".format(ColorKey.YELLOW, self.get_acceptable_input_str(), ColorKey.NORMAL))
else:
return input_text
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/python3
# Copyright (c) 2025 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
#
# Java property file support is provided because for VC 9.0 the file:
# /usr/lib/vmware-sso/vmware-sts/conf/server.xml
# Indicated in lib/constants.py by:
# STS_SERVER_CONFIG_FILE_PATH
# has been changed to the Java property file:
# /usr/lib/vmware-sso/vmware-sts/conf/catalina.properties
# Indicated in lib/constants.py by:
# STS_SERVER_CONFIG_PROPERTY_FILE_PATH
def load_property_file(file=None):
"""
Load Java property list file.
"""
properties = {}
with open(file) as f:
for line in f:
line = line.strip()
if line.startswith('#') or '=' not in line:
continue
keypath, value = line.split('=', 1)
# The following commented out code loads the property file using
# nested dictionaries, following the hierarchy defined by the
# dotted key path. However, current usage doesn't require that, so
# for now we simply load the file as a simple dictionary to
# simplify key lookups with the current code.
#
# keys = keypath.split('.')
# keyDict = properties
# for key in keys[:-1]:
# if key not in keyDict:
# keyDict[key] = {}
# keyDict = keyDict[key]
# keyDict[keys[-1]] = value
properties[keypath] = value
return properties
if __name__ == '__main__':
# Simple command-level driver for testing Java property file parsing.
import argparse
parser = argparse.ArgumentParser()
parser.add_argument(
'--file',
help='Java property file',
required=True
)
options = parser.parse_args()
properties = load_property_file(file=options.file)
for key, value in properties.items():
print("%s: %s" % (key, value))
+266
View File
@@ -0,0 +1,266 @@
# Copyright (c) 2024 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import copy
import hashlib
import json
import logging
import ldap3 as ldap
from lib.environment import Environment
from lib.exceptions import LdapException
from lib.execution_replay import ReplayContext
logger = logging.getLogger(__name__)
class Ldap:
@staticmethod
def open_ldap_connection(node, user_dn, password):
"""
Open ldap connection to the ldap server provided with
ldap admin user credentials.
:param node: hostname of the ldap server
:param user_dn: User DN for authentication
:param password: Password for authentication
:return:
"""
logger.info("Opening connection to {} with user {}".format(node, user_dn))
server = ldap.Server(get_uri_from_hostname(node), get_info=ldap.ALL)
env = Environment.get_environment()
is_remote_exec = env.get_value('VCERT_REMOTE_EXEC') is True
is_replay = env.get_value('VCERT_REMOTE_EXEC_REPLAY') is True
is_capture = env.get_value('VCERT_REMOTE_EXEC_CAPTURE') is True
if is_remote_exec and (is_replay or is_capture):
context = ReplayContext.get_replay_context()
ldap_connection = LdapConnectionReplay(context, server, user_dn, password)
else:
ldap_connection = ldap.Connection(server, user=user_dn, password=password)
if not ldap_connection.bind():
logger.error("Failed to do LDAP bind with host %s with %s error",
node, ldap_connection.result['description'])
raise LdapException(ldap_connection.result['result'], ldap_connection.result['description'])
return ldap_connection
@staticmethod
def close_ldap_connection(ldap_connection) -> None:
"""
Close the ldap bind connection
:param ldap_connection:ldap connection to be closed
:return:
"""
logger.info("Closing LDAP connection")
ldap_connection.unbind()
if not ldap_connection.closed:
logger.error("Error closing connection. Error Msg: %s", ldap_connection.result["message"])
@staticmethod
def ldap_search(ldap_connection, base_dn, ldap_filter, ldap_scope, ldap_attributes) -> bool:
"""
This method takes the ldap connection, base dn, filter , scope
and list of ldap attributes to be returned.
:param ldap_connection connection to ldap server
:param base_dn dn where the search starts
:param ldap_filter to filter the search the results
:param ldap_scope scope of the search
:param ldap_attributes list of ldap attributes to be returned
"""
logger.info("LDAP search with\n base DN:%s\n filter: %s\n scope: %s\n attributes: %s",
base_dn, ldap_filter, str(ldap_scope), str(" ".join(ldap_attributes)))
if ldap_connection is None:
raise LdapException("-1", "No LDAP connection")
else:
if not ldap_connection.bind():
raise LdapException(ldap_connection.result['result'], ldap_connection.result['description'])
result = ldap_connection.search(base_dn, ldap_filter, ldap_scope, attributes=ldap_attributes)
# When a filter doesn't match any entry result will be false
if result or (ldap_connection.result and ldap_connection.result['result'] == 0):
return True
logger.error("LDAP search failed. Error message: %s", ldap_connection.result["message"])
return False
@staticmethod
def get_attribute(ldap_entry, attribute) -> str:
"""
get the attribute value for a given ldap entry
This function can only be used for single value attributes.
:param ldap_entry: ldap entry
:param attribute: attribute to be returned
:return: value which is string
"""
val = ldap_entry['attributes'][attribute]
if isinstance(val, str):
return val
return val[0]
@staticmethod
def ldap_delete(ldap_connection, entry_dn):
"""
This method takes ldap connection and deletes a given entry
:param ldap_connection: LDAP connection
:param entry_dn: entry DN to be delete
:return:
"""
logger.info("LDAP delete with DN: %s", entry_dn)
if ldap_connection is None:
logger.debug("No ldap connection")
raise LdapException("-1", "No LDAP connection")
else:
if not ldap_connection.bind():
raise LdapException(ldap_connection.result['result'], ldap_connection.result['description'])
if ldap_connection.delete(entry_dn):
logger.debug("Deleted entry DN: %s", entry_dn)
return True
else:
if ldap_connection.result['result'] == 32:
logger.debug("Entry to be deleted %s doesn't exist", entry_dn)
return True
logger.error("Error deleting entry DN: %s, error code %s error msg %s",
entry_dn, ldap_connection.result['result'],
ldap_connection.result['description'])
return False
@staticmethod
def ldap_modify(ldap_connection, dn, attribute, operation, value=None):
"""
This method takes ldap connection, DN and single attribute modification
for that particular DN
:param ldap_connection:
:param dn:
:param attribute:
:param operation:
:param value:
:return:
"""
logger.info("LDAP modify for DN %s with\n attribute: %s\n operation: %s\n value: %s",
dn, attribute, operation, value)
if ldap_connection is None:
logger.debug("No ldap connection")
raise LdapException("-1", "No Ldap connection")
if not ldap_connection.bind():
raise LdapException(ldap_connection.result['result'], ldap_connection.result['description'])
if value is None:
value = []
changes = [(operation, value)] if type(value) is list else [(operation, [value])]
result = ldap_connection.modify(dn, {attribute: changes})
if result:
logger.info("Modified DN:%s", dn)
return True
logger.error("Modifying entry %s failed with error code %s, description %s",
dn, ldap_connection.result['result'], ldap_connection.result['description'])
return False
@staticmethod
def ldap_add(ldap_connection, dn, object_class, attributes):
"""
Add new LDAP entry with DN and attributes {attributes}
:param ldap_connection: LDAP connection
:param dn: DN to add
:param attributes: attributes
"""
logger.info("LDAP add for DN %s with\n attributes: %s\n", dn, str(attributes))
if ldap_connection is None:
logger.debug("No ldap connection")
raise LdapException("-1", "No Ldap connection")
if not ldap_connection.bind():
raise LdapException(ldap_connection.result['result'], ldap_connection.result['description'])
result = ldap_connection.add(dn, object_class, attributes)
if result:
logger.info("Added DN:%s", dn)
return True
logger.error("Adding entry %s failed with error code %s, description %s",
dn, ldap_connection.result['result'], ldap_connection.result['description'])
return False
class LdapConnectionReplay(object):
"""
Class for capturing and replaying the previous LDAP operation results
"""
def __init__(self, context: ReplayContext, server, user_dn, password):
self.context = context
self.server = server
self.user_dn = user_dn
self.password = password
self.credential_hash = hashlib.sha1("{}:{}".format(user_dn, password).encode('utf-8')).hexdigest()
self.ldap_connection = ldap.Connection(server, user_dn, password) if self.context.is_capturing else None
self.closed = True
self.result = None
self.response = None
def operation_wrapper(self, name, func, args, pass_connection=True):
final_args = [name]
final_args.extend(args)
if self.context.is_replaying:
prev_result = self.context.get_execution_result('ldap', final_args, self.credential_hash)
if prev_result is not None:
return_value, result_text, _ = prev_result
result = ReplayContext.decode_bytes(json.loads(result_text))
self.result = result['_result_']
self.response = result['_response_']
self.closed = result['_closed_']
if self.ldap_connection and func == self.ldap_connection.unbind:
self.ldap_connection.unbind()
return return_value
return_value = func(self.ldap_connection, *args) if pass_connection else func(*args)
self.result = self.ldap_connection.result
self.response = self.ldap_connection.response
self.closed = self.ldap_connection.closed
result = dict()
result['_result_'] = ReplayContext.encode_bytes(copy.deepcopy(self.result))
result['_response_'] = ReplayContext.encode_bytes(copy.deepcopy(self.response))
result['_closed_'] = self.closed
try:
result_text = json.dumps(result, skipkeys=True)
except TypeError as te:
raise LdapException("Unserializable object: {}".format(result), str(te))
self.context.store_result('ldap', final_args, self.credential_hash, return_value, result_text, None)
return return_value
def bind(self):
func = self.ldap_connection.bind if self.ldap_connection else None
return self.operation_wrapper('bind', func, [], False)
def unbind(self):
func = self.ldap_connection.unbind if self.ldap_connection else None
return self.operation_wrapper('unbind', func, [], False)
def search(self, search_base, search_filter, search_scope, attributes):
args = [search_base, search_filter, search_scope, attributes]
return self.operation_wrapper('search', Ldap.ldap_search, args)
def modify(self, dn, changes):
attribute = list(changes.keys())[0]
operation, values = changes[attribute][0]
args = [dn, attribute, operation, values]
return self.operation_wrapper('modify', Ldap.ldap_modify, args)
def delete(self, dn):
return self.operation_wrapper('delete', Ldap.ldap_delete, [dn])
def add(self, dn, object_class, attributes):
return self.operation_wrapper('add', Ldap.ldap_add, [dn, object_class, attributes])
def get_uri_from_hostname(hostname):
return "ldap://{}".format(hostname)
def get_domain_dn(domain):
if '@' in domain:
domain = domain.split('@', 2)[1]
domain_parts = domain.split('.')
return "dc={}".format(",dc=".join(domain_parts))
def get_user_dn(user_upn: str):
username, domain = tuple(user_upn.split('@'))
domain_dn = get_domain_dn(domain)
return "cn={},cn=users,{}".format(username, domain_dn)
+438
View File
@@ -0,0 +1,438 @@
# Copyright (c) 2024-2025 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import glob
import os
import yaml
import logging
from lib.console import print_header, print_text, ColorKey, set_text_color, print_text_error
from lib.environment import Environment
from lib.exceptions import MenuExitException, OperationFailed
from lib.host_utils import get_config_file_path
from lib.input import MenuInput
from lib.operation import Operation
from lib.text_utils import translate_text
from lib.vmdir import get_identity_sources
logger = logging.getLogger(__name__)
class MenuItem(object):
"""
Class for holding menu item information
"""
def __init__(self, text, method, method_args=None, key=None, is_disabled=False,
is_hidden=False, is_default=False, use_label_as_key=False):
"""
Initialize MenuItem object
:param text: label for the menu item when displayed as part of menu
:param method: the method to be called if the menu item is executed
:param method_args: arguments to be supplied to method
:param key: specify menu key explicitly. If this key not specified,
the key will be generated using a sequenced number
:param is_disabled: show menu as disabled menu item
:param is_default: use this menu item if the input return empty key
:param use_label_as_key: use the label as the key for default input
"""
self.text = text
self.method = method
self.method_args = method_args
self.key = key
self.is_disabled = is_disabled
self.is_hidden = is_hidden
self.is_default = is_default
self.use_label_as_key = use_label_as_key
class Menu(object):
"""
Class for representing a single menu dialog, providing the method to
define and show menu list, the handling of accepted inputs and the
execution of menu item
"""
def __init__(self):
self.title = ''
self.sub_title = ''
self.items = []
self.input_text = None
self.run_once = False
def set_menu_options(self, title, sub_title=None, run_once=False, input_text=None):
"""
Set the menu option: menu title and the input text
"""
self.title = title
self.sub_title = sub_title
self.input_text = input_text
self.run_once = run_once
def add_menu_item(self, label, method=None, method_args=None, key=None, is_disabled=False,
is_hidden=False, is_default=False, use_label_as_key=False):
"""
Add menu item entry
:param label: label for the menu item when displayed as part of menu
:param method: method object to be called if the menu item is executed
:param method_args: arguments to be supplied to method
:param key: specify menu key explicitly. If this key not specified,
the key will be auto-generated using sequence number
:param is_disabled: show menu as disabled menu item
:param is_hidden: if True, hidden the menu entry
:param is_default: use this menu item if the input return empty key
:param use_label_as_key: use the label as the key for default input
"""
# validate that the new menu item entry doesn't have conflict with the existing ones
if key is not None:
key = key.upper()
for item in self.items:
if key is not None and key == item.key:
raise ValueError("Duplicating menu item key: {}".format(key))
if is_default and item.is_default:
raise ValueError('Duplicating default menu item')
if is_hidden and key is None:
raise ValueError('Hidden menu item requires explicit key value')
self.items.append(MenuItem(label, method, method_args, key, is_disabled, is_hidden,
is_default, use_label_as_key))
def get_all_menu_keys(self):
"""
Get all menu keys defined or generated for the current menu entries
:returns all menu keys
"""
user_keys = []
for item in self.items:
if item.key is not None:
user_keys.append(item.key)
keys = [str(i) for i in range(1, len(self.items) - len(user_keys) + 1)]
keys.extend(user_keys)
return keys
def get_all_menu_keys_string(self) -> str:
"""
Get all menu keys as a single string
The auto-generated keys will be simplified using range expression.
Example: When get_all_menu_keys() returns ['1', '2', '3', 'E'],
this method will return '1-3, E'
:return meny keys in a single-line string
"""
keys = []
for item in self.items:
if item.key is not None:
keys.append(item.key)
num_others = len(self.items) - len(keys)
if num_others == 1:
keys.insert(0, "1")
elif num_others > 1:
keys.insert(0, "{}-{}".format(1, num_others))
return ", ".join(keys)
def get_menu_item(self, key):
"""
Get menu item using key {key}
:param key: key to be used to search the menu item
:return: MenuItem object
"""
key = key.upper()
item_key = 1
for item in self.items:
if item.key is None:
if key == str(item_key):
return item
else:
item_key += 1
elif item.key == key:
return item
return None
def get_input(self):
"""
Get user input from console
:return: key entered by user
"""
keys = self.get_all_menu_keys()
# get default key
default_key = None
seq = 0
for item in self.items:
if not item.key:
seq += 1
if item.is_default:
default_key = item.key if item.key else str(seq)
default_key_text = item.text if item.use_label_as_key else default_key
Environment.get_environment().get_value('CURRENT_MENU')['__DEFAULT__'] = default_key
break
# compose default input text if required
input_text = self.input_text
if self.input_text is None:
if default_key is not None:
input_text = "Select an option [{key}]: ".format(key=default_key_text)
else:
input_text = 'Select an option: '
menu_input = MenuInput(translate_text(input_text), acceptable_inputs=keys, default_input=default_key)
return menu_input.get_input()
def show_menu(self):
"""
Show the menu list
"""
if self.title:
print()
print_header(self.title)
if self.sub_title:
print_text(self.sub_title)
seq = 1
for item in self.items:
if item.is_hidden:
continue
key = item.key
if not key:
key = str(seq)
seq += 1
if item.is_disabled:
set_text_color(ColorKey.YELLOW)
print_text("{:>2}. {}".format(key, translate_text(item.text)))
if item.is_disabled:
set_text_color(ColorKey.NORMAL)
def run(self):
"""
Execute the menu: show the menu list, get user input, and execute
the selected menu item
"""
Environment.get_environment().set_value('CURRENT_MENU', dict())
while True:
self.show_menu()
print()
try:
key = self.get_input()
item = self.get_menu_item(key)
except KeyboardInterrupt:
raise MenuExitException('KeyboardInterrupt exception')
logger.info('Running menu item: {}'.format(item))
if item is None:
raise RuntimeError('Menu item not found')
elif item.is_disabled:
print()
print_text_error('Operation is not available')
return
elif item.method == Menu.run_navigation_exit:
raise MenuExitException('Exit requested')
elif item.method == Menu.run_navigation_return:
return
try:
if item.method_args is not None:
item.method(**item.method_args)
else:
item.method()
except OperationFailed as e:
print()
print_text_error('Operation failed: {}'.format(str(e)))
except KeyboardInterrupt:
raise MenuExitException('KeyboardInterrupt exception')
if self.run_once:
return
@staticmethod
def load_menu_item_from_file(config_file):
"""
Load menu item definition from yaml config file
:param config_file: file to be loaded
:return: menu item type, label, and enable condition string
"""
config_file = get_config_file_path(config_file)
with open(config_file, 'r') as file:
item_config = yaml.safe_load(file)
label = item_config.get('label')
condition = item_config.get('condition')
if label is None:
label = item_config['title']
return item_config['type'], label, condition
@staticmethod
def load_menu_items_from_files(config_files):
"""
Load multiple menu item definitions from yaml config files, specified
using a file pattern (glob)
:param config_files: file pattern to be loaded.
:return: list of a tuple of menu item type, label, and condition
"""
items = []
env = Environment.get_environment()
config_files = config_files if os.path.isabs(config_files) \
else "{}/{}".format(env.get_value('SCRIPT_DIR'), config_files)
for config in sorted(glob.glob(config_files)):
with open(config, 'r') as file:
item_config = yaml.safe_load(file)
logger.info('Configuration from {} is: {}'.format(config, item_config))
label = item_config.get('label')
condition = item_config.get('condition')
if label is None:
label = item_config['title']
items.append((item_config['type'], label, config, condition))
return items
@staticmethod
def load_menu_from_config(config_file):
"""
Load a full menu context from config file
:param config_file: a yaml config file to be loaded
:return: Menu object created from the config file
"""
env = Environment.get_environment()
menu = Menu()
config_file = get_config_file_path(config_file)
with open(config_file, 'r') as file:
menu_config = yaml.safe_load(file)
title = menu_config['title']
sub_title = menu_config.get('sub_title', '')
run_once = menu_config.get('run_once', False)
input_text = None
if menu_config.get('input'):
input_text = menu_config['input']['text']
menu.set_menu_options(title, sub_title, run_once, input_text)
for item_config in menu_config['items']:
item_type = item_config['type']
item_label = item_config.get('label')
is_default = item_config.get('default') is True
condition = item_config.get('condition')
is_disabled = env.get_value(condition) is not True if condition else False
is_hidden = item_config.get('hidden') is True
use_label_as_key = item_config.get('use_label_as_key', False)
key = item_config.get('key')
if item_type == 'single':
submenu_config = item_config['config']
submenu_type, submenu_label, submenu_condition = Menu.load_menu_item_from_file(submenu_config)
if item_label is None:
item_label = submenu_label
if not is_disabled:
is_disabled = env.get_value(submenu_condition) is not True if submenu_condition else False
handler = Menu.run_menu_from_config if submenu_type == 'menu' else Menu.run_operation_from_config
menu.add_menu_item(item_label, handler, {'config': submenu_config}, key=key,
is_disabled=is_disabled, is_hidden=is_hidden, is_default=is_default)
elif item_type == 'multiple':
items = Menu.load_menu_items_from_files(item_config['config'])
for submenu_type, submenu_label, submenu_config, submenu_condition in items:
is_disabled = env.get_value(submenu_condition) is not True if submenu_condition else False
handler = Menu.run_menu_from_config \
if submenu_type == 'menu' else Menu.run_operation_from_config
menu.add_menu_item(submenu_label, handler, {'config': submenu_config},
is_disabled=is_disabled)
elif item_type == 'navigation:exit':
menu.add_menu_item(item_label, Menu.run_navigation_exit, key=key, is_disabled=is_disabled,
is_hidden=is_hidden, is_default=is_default)
elif item_type == 'navigation:return':
menu.add_menu_item(item_label, Menu.run_navigation_return, key=key, is_disabled=is_disabled,
is_hidden=is_hidden, is_default=is_default, use_label_as_key=use_label_as_key)
return menu
@staticmethod
def get_menu_label_and_condition_from_config(config):
"""
Obtain menu label and condition only from a menu config file
:param config: config file to be loaded
:return: tuple of menu item label and condition
"""
config = get_config_file_path(config)
with open(config, 'r') as file:
menu_config = yaml.safe_load(file)
label = menu_config.get('label')
if label is None:
label = menu_config['title']
condition = menu_config.get('condition')
return label, condition
@staticmethod
def run_menu_from_config(config):
"""
Method to be used by MenuItem that load another menu (submenu)
:param config: config to be load in the submenu
"""
menu = Menu.load_menu_from_config(config)
menu.run()
@staticmethod
def run_operation_from_config(config):
"""
Method to be used by MenuItem that execute operation
:param config: config to be loaded to run the operation
"""
operation = Operation.load_operation_from_config(config)
try:
operation.run()
except OperationFailed as e:
print()
print_text_error("Operation failed: {}".format(str(e)))
@staticmethod
def run_navigation_exit():
"""
Method to be used by MenuItem that cause application exit
"""
pass
@staticmethod
def run_navigation_return():
"""
Method to be used by MenuItem that cause return to parent menu
"""
pass
@staticmethod
def set_menu_conditions():
Menu.set_ldaps_condition()
Menu.set_machine_ssl_csr_condition()
Menu.set_backup_store_condition()
@staticmethod
def set_ldaps_condition():
env = Environment.get_environment()
identity_sources = get_identity_sources(use_machine_account=True)
value = True if identity_sources else False
env.set_value('HAS_LDAPS_IDENTITY_SOURCE', value)
@staticmethod
def set_machine_ssl_csr_condition():
# XXX Issue 43: Import lib.vecs in the function to break circular
# dependency with lib.vecs which imports lib.menu.
from lib.vecs import get_certificate_aliases
env = Environment.get_environment()
machine_ssl_aliases = get_certificate_aliases('MACHINE_SSL_CERT')
value = True if '__MACHINE_CSR' in machine_ssl_aliases else False
env.set_value('HAS_MACHINE_SSL_CSR', value)
@staticmethod
def set_backup_store_condition():
# XXX Issue 43: Import lib.vecs in the function to break circular
# dependency with lib.vecs which imports lib.menu.
from lib.vecs import get_store_list
env = Environment.get_environment()
backup_stores = ['BACKUP_STORE', 'BACKUP_STORE_H5C']
vecs_stores = get_store_list()
value = any(store in vecs_stores for store in backup_stores)
env.set_value('HAS_BACKUP_STORE', value)
+230
View File
@@ -0,0 +1,230 @@
# Copyright (c) 2024-2025 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import glob
import importlib
import logging
import os
import sys
import yaml
from lib.console import print_header, print_text_error
from lib.environment import Environment
from lib.exceptions import OperationFailed, CommandExecutionError
from lib.host_utils import get_config_file_path
from lib.input import MenuInput
logger = logging.getLogger(__name__)
class ValueSource(object):
"""
Class for providing operation arguments as part of config file
"""
def __init__(self, value):
self.value = value
def get_value(self):
return self.value
class EnvironmentSource(object):
"""
Class for providing operation argument from environment variable
"""
def __init__(self, key):
self.key = key
def get_value(self):
"""
Return value from environment
"""
env = Environment.get_environment()
return env.get_value(self.key)
class InputSource(object):
"""
Class for providing operation arguments via user input dialog
"""
def __init__(self, text, acceptable_inputs, default_input=None, allow_empty_input=True, case_insensitive=False,
masked=False):
"""
InputSource constructor. All arguments will be passed to MenuInput
"""
self.text = text
self.acceptable_inputs = acceptable_inputs
self.default_input = default_input
self.allow_empty_input = allow_empty_input
self.case_insensitive = case_insensitive
self.masked = masked
def get_value(self):
"""
Return value obtained from MenuInput execution
"""
source_input = MenuInput(self.text, self.acceptable_inputs, self.default_input, self.allow_empty_input,
self.case_insensitive, self.masked)
return source_input.get_input()
class OperationArgument(object):
"""
Class for defining operation argument
"""
def __init__(self, name, source):
self.name = name
self.source = source
class Operation(object):
"""
Class for defining operation object
"""
def __init__(self, title, module_name, method_name, condition_key):
"""
Operation constructor method
:param title: text to be displayed when the operation is executed
:param module_name: module to be load to find the method for this operation
:param method_name: method to be called for this operation
:param condition_key: key to be used to evaluate if the operation is disabled
"""
env = Environment.get_environment()
sys.path.append(env.get_value('SCRIPT_DIR'))
self.title = title
self.module = importlib.import_module(module_name)
obj = self.module
for attr_name in method_name.split('.'):
obj = getattr(obj, attr_name)
self.method = obj
self.arguments = []
self.condition_key = condition_key
def add_argument(self, name, source):
self.arguments.append(OperationArgument(name, source))
def get_argument_values(self):
"""
Get operation arguments as dict. This method will also reset CURRENT_MENU
environment with this mapping
"""
args = dict()
env = Environment.get_environment()
env.set_value('CURRENT_MENU', args)
for arg in self.arguments:
value = arg.source.get_value()
args[arg.name] = value
return args
def is_disabled(self):
"""
Check if the operation is disabled
"""
if self.condition_key:
return Environment.get_environment().get_value(self.condition_key) is not True
else:
return False
def run(self):
"""
Execute the operation
"""
if self.is_disabled():
print_text_error('Operation is disabled!')
print()
return
if self.title:
print_header(self.title)
try:
return self.method(**self.get_argument_values())
except CommandExecutionError as e:
raise OperationFailed(str(e))
class OperationGroup(object):
"""
Class for defining operation that aggregate other operations
"""
def __init__(self, title):
self.title = title
self.operations = []
def add_operation(self, op):
self.operations.append(op)
def run(self):
for op in self.operations:
op.run()
@staticmethod
def load_operation_from_config_obj(config):
"""
Load operation from loaded config object
:param config: config file to be loaded
"""
entry_point = config['entry_point']
operation = Operation(config.get('title'), entry_point['module'], entry_point['method'],
config.get('condition'))
arguments = config.get('arguments')
if arguments:
for arg in config.get('arguments'):
arg_name = arg['name']
if arg.get('value') is not None:
operation.add_argument(arg_name, ValueSource(arg['value']))
continue
source_config = arg['source']
if source_config['type'] == 'input':
operation.add_argument(
arg_name,
InputSource(text=source_config['input_text'],
acceptable_inputs=source_config.get('acceptable_inputs'),
default_input=source_config.get('default_input'),
case_insensitive=(source_config.get('case_insensitive') is True),
masked=(source_config.get('masked') is True)))
elif source_config['type'] == 'environment':
operation.add_argument(arg_name, EnvironmentSource(source_config['key']))
return operation
@staticmethod
def load_operation_from_config(config_file):
"""
Load operation from a yaml config file
:param config_file: config file to be loaded
:return: Return Operation or OperationGroup object
"""
env = Environment.get_environment()
config_file = get_config_file_path(config_file)
logger.info("Loading operation from config file {}".format(config_file))
with open(config_file, 'r') as file:
config = yaml.safe_load(file)
if config['type'] == "operation":
return Operation.load_operation_from_config_obj(config)
op_group = Operation.OperationGroup(config.get('title'))
for operation in config['operations']:
if operation['type'] == 'single':
if operation.get('config'):
op = Operation.load_operation_from_config(operation['config'])
else:
op = Operation.load_operation_from_config_obj(operation)
op_group.add_operation(op)
continue
# logger.info("Parsing multiple operations in {}".format(glob.glob(operation['config'])))
# Only use the SCRIPT_DIR environment setting if the config
# path isn't absolute.
opconfig = operation['config']
if os.path.isabs(opconfig):
globPath = opconfig
else:
globPath = os.path.join(env.get_value('SCRIPT_DIR'), opconfig)
for config_file in sorted(glob.glob(globPath)):
op = Operation.load_operation_from_config(config_file)
op_group.add_operation(op)
return op_group
+119
View File
@@ -0,0 +1,119 @@
# Copyright (c) 2024 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import logging
import sys
from lib.command_runner import CommandRunner
from lib.console import print_text_warning
from lib.host_utils import get_file_contents
from lib.text_utils import TextFilter
from lib.vmdir import get_vmdir_state
SERVICE_CONTROL_CLI = '/usr/bin/service-control'
VMON_CLI = '/usr/sbin/vmon-cli'
logger = logging.getLogger(__name__)
def get_vmon_service_profile():
"""
Get vmon service profile
"""
vmon_service_profile = get_file_contents('/storage/vmware-vmon/defaultStartProfile')
return ['--vmon-profile', 'HAActive'] if 'HACore' in vmon_service_profile else ['--all']
def list_vmware_services():
"""
Get a list of services present in the VC
:param:
:return: List of services
"""
output = CommandRunner(SERVICE_CONTROL_CLI, '--list-services').run_and_get_output().strip()
services = TextFilter(output).cut(fields=[0]).get_lines()
return services
def start_vmware_services(service=None):
"""
Start VMware services
:param service: to be started. If it's None, all service will be restarted
:return: True if service(s) are started successfully. Otherwise, False
"""
if service is None:
service_profile = get_vmon_service_profile()
ret_code, _, stderr = CommandRunner(SERVICE_CONTROL_CLI, '--start', *service_profile).run()
else:
ret_code, _, stderr = CommandRunner(SERVICE_CONTROL_CLI, '--start', service).run()
if ret_code != 0:
logger.error("Failed to start VMware services: {}".format(stderr))
return False
return True
def stop_vmware_services(service=None):
"""
Stop VMware service
:param service: to be stopped. If it's None, all service will be stopped
:return: True if service(s) are started successfully. Otherwise, False
"""
if service is None:
service_profile = get_vmon_service_profile()
ret_code, _, stderr = CommandRunner(SERVICE_CONTROL_CLI, '--stop', *service_profile).run()
else:
ret_code, _, stderr = CommandRunner(SERVICE_CONTROL_CLI, '--stop', service).run()
if ret_code != 0:
logger.error("Failed to stop VMware services: {}".format(stderr))
return False
return True
def check_services_status():
services = [['envoy', True, True],
['rhttpproxy', True, True],
['vmafdd', False, True],
['vmdird', False, True],
['vmware-vpostgres', True, False]]
for service_info in services:
service_name = service_info[0]
managed_by_vmon = service_info[1]
exit_if_stopped = service_info[2]
if not is_service_running(service_name, managed_by_vmon):
if exit_if_stopped:
print_text_warning('The {} service is not running, the script cannot continue. Exiting...'.format(service_name))
sys.exit(1)
else:
print_text_warning('\n-------------------------!!! Warning !!!-------------------------')
print_text_warning('The {} service is not running, which may impact certain script operations.'.format(service_name))
print_text_warning('For best results, please start the {} service.'.format(service_name))
elif service_name == 'vmdird':
vmdir_state = get_vmdir_state()
logger.info('The VMware Directory service state is: {}'.format(vmdir_state))
if vmdir_state not in ['Normal', 'Standalone']:
print_text_warning('The VMware Directory service is in the following state: {}. The script cannot continue. Exiting...'.format(vmdir_state))
sys.exit(1)
return
def is_service_running(service, managed_by_vmon=True):
if managed_by_vmon:
# determining status of services managed by vMon is quicker using vmon-cli as oppsed to service-control
service_output = CommandRunner(VMON_CLI, '-s', '{}'.format(service)).run_and_get_output()
run_state = TextFilter(service_output).start_with('RunState').cut(':', [1]).get_text().strip()
logger.info('Service {} running state: {}'.format(service, run_state))
if run_state == 'STARTED':
return True
else:
return False
else:
# determining status of services managed by systemd is quicker using systemctl as oppsed to service-control
service_output = CommandRunner('systemctl', 'status', '{}'.format(service)).run_and_get_output()
run_state = TextFilter(service_output).contain('Active:').cut(':', [1]).cut(' ', [1]).get_text()
logger.info('Service {} running state: {}'.format(service, run_state))
if run_state == 'active':
return True
else:
return False
+274
View File
@@ -0,0 +1,274 @@
# Copyright (c) 2024 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import re
from lib.environment import Environment
class TextFilter(object):
"""
A utility class for filtering multi-line text. The filtering methods in this
class will return self to allow method chaining
Example of usage:
result = TextFilter(text).contain(filter).head(1).remove(unwanted_chars).get_text()
"""
def __init__(self, text):
self.lines = text.splitlines()
self.before_lines = 0
self.after_lines = 0
self.invert_match = False
def get_text(self):
"""
Return the result as a string, joined by '\n'
"""
return "\n".join(self.lines)
def get_lines(self):
"""
Return the current filtering result as a string list
"""
return self.lines
def set_options(self, before_lines=0, after_lines=0, invert_match=False):
"""
Set the matching option
:param before_lines: the result will also contain the before_lines lines
before the matched line
:param after_lines: the result will also contain the after_lines lines
after the matched lines
:param invert_match: If True, the match operations will return lines that
that don't match to the search pattern
"""
self.before_lines = before_lines
self.after_lines = after_lines
self.invert_match = invert_match
return self
def reset_options(self):
"""
Reset the matching option
"""
self.before_lines = self.after_lines = 0
self.invert_match = False
return self
def mod_match_result(self, match_result):
return not match_result if self.invert_match else match_result
def get_lines_with_extended_indices(self, indices):
"""
Extend the matching lines indices by applying before_lines and after_lines
matching options
:param indices: indices to extend
:return: extended indices
"""
if self.before_lines > 0 or self.after_lines > 0:
new_indices = []
for index in indices:
if self.before_lines > 0:
begin_index = index - self.before_lines
if begin_index < 0:
begin_index = 0
new_indices.extend([idx for idx in range(begin_index, index)])
if self.after_lines > 0:
end_index = index + self.after_lines + 1
if end_index > len(self.lines):
end_index = len(self.lines)
new_indices.extend([idx for idx in range(index + 1, end_index)])
indices.extend(new_indices)
indices = sorted(set(indices))
return [line for idx, line in enumerate(self.lines) if idx in indices]
def match(self, pattern):
"""
Filter lines using regular expression match
:param pattern: regular expression pattern for matching the lines
"""
indices = [idx for idx, line in enumerate(self.lines)
if self.mod_match_result(re.match(pattern, line))]
self.lines = self.get_lines_with_extended_indices(indices)
return self
def contain(self, keyword):
"""
Filter the lines using simple keywords
:param keyword: keyword for matching the lines
"""
indices = [idx for idx, line in enumerate(self.lines)
if self.mod_match_result(keyword in line)]
self.lines = self.get_lines_with_extended_indices(indices)
return self
def start_with(self, text):
"""
Filter the lines that started with {text}
:param text: text for filtering
"""
indices = [idx for idx, line in enumerate(self.lines)
if self.mod_match_result(line.find(text) == 0)]
self.lines = self.get_lines_with_extended_indices(indices)
return self
def head(self, count=1):
"""
Include only the first {count} lines to be included in the result
:param count: total of lines to be included in the result. If the value is
a negative value between -1 and -[number of lines], it will include all
lines except of the last {count} lines at the end
"""
if count < 0:
count += len(self.lines)
if count < 0:
count = 0
self.lines = [line for i, line in enumerate(self.lines) if i < count]
return self
def tail(self, count=1):
"""
Includes only the last {count} lines
:param count: total of lines to be included. If the value is a negative
value between -1 and -[#lines], it will include all lines except the
first {count} lines at the beginning
:return:
"""
length = len(self.lines)
if count < 0:
count += length
self.lines = [line for i, line in enumerate(self.lines) if i >= length - count]
return self
def match_block(self, pattern_begin, pattern_end, concatenate=False):
"""
Filter the lines using begin and end pattern
:param pattern_begin: the pattern for starting the matching block
:param pattern_end: the pattern for stopping the matching block
:param concatenate: if True, the matching block will be concatenated
as a single string joined using `\n' character
"""
lines = []
block = []
matching = False
for line in self.lines:
if not matching and re.match(pattern_begin, line):
matching = True
block = []
if matching:
block.append(line)
if matching and re.match(pattern_end, line):
matching = False
if concatenate:
lines.append('\n'.join(block))
else:
lines.extend(block)
self.lines = lines
return self
def apply(self, method):
"""
Apply method on all lines. The lines will be replaced using the values returned
by the method
:param method: method to be applied
"""
self.lines = [method(line) for line in self.lines]
return self
def replace(self, keyword1, keyword2):
"""
Update the lines by replacing keywords
:param keyword1: keyword to be replaced
:param keyword2: keyword to be used for replacing
"""
self.lines = [line.replace(keyword1, keyword2) for line in self.lines]
return self
def remove(self, keyword):
"""
Update all lines by removing {keyword}
:param keyword: text to be removed
"""
return self.replace(keyword, '')
def remove_white_spaces(self):
"""
Remove white spaces in all lines
"""
return self.apply(remove_white_spaces_method)
def cut(self, delimiter=' ', fields=None, new_delimiter=' ', include_empty=False):
"""
Split the line using delimiter and include only the required fields.
This method tries to mimics cut(1) command
:param delimiter: The delimiter to be used for split the line
:param fields: list of indices to be included (0 based index)
:param new_delimiter: If specified, the result will be concenated back
using the new_delimiter
:param include_empty: If True, the result may also contain empty lines
when the request fields are not available (default: False)
"""
lines = []
if fields is None:
fields = []
for line in self.lines:
result = []
words = line.split(delimiter)
for idx in fields:
if idx == -1:
idx = len(words) - 1
if idx < len(words):
result.append(words[idx])
if result or include_empty:
lines.append(new_delimiter.join(result))
self.lines = lines
return self
def get_count(self):
"""
Get the count of lines in the current result
"""
return len(self.lines)
def dump(self):
"""
Dump the current contents. This can be use for debugging purpose
"""
print('Contents:', '\n'.join(self.lines), sep='\n')
return self
def remove_white_spaces_method(text):
"""
Method for removing white spaces to be specified to TextUtil.apply
"""
return re.sub(r'\s+', '', text)
def translate_text(*text, sep=''):
"""
Reformat text using mapping in the environment variables
:param text: String parameters
:param sep: Separator if the text contains multiple string parameters
:return: reformatted text
"""
text = sep.join(text)
if text and '{' in text:
env_map = Environment.get_environment().get_map()
text = text.format_map(env_map)
return text
+155
View File
@@ -0,0 +1,155 @@
# Copyright (c) 2024 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
from lib.command_runner import CommandRunner
from lib.host_utils import get_vc_version, VcVersion
from lib.text_utils import TextFilter
PSQL_CLI = '/usr/bin/psql'
def get_extension_thumbprints(extensions):
"""
Get extension thumbprints from VCDB
:param extensions: list of extensions to fetch
:return: dictionary of tuple (extension name, thumbprint, cert PEM)
"""
query = "SELECT ext_id, thumbprint FROM vpx_ext WHERE ext_id IN ('{}') ORDER BY ext_id" \
.format('\', \''.join(extensions))
output = CommandRunner(PSQL_CLI, '-d', 'VCDB', '-U', 'postgres',
'-c', query, '-t').run_and_get_output()
extensions = TextFilter(output).contain('|').cut(delimiter='|', fields=[0]).remove_white_spaces().get_lines()
thumbprints = TextFilter(output).contain('|').cut(delimiter='|', fields=[1]).remove_white_spaces().get_lines()
result = dict()
for index, extension in enumerate(extensions):
if get_vc_version() >= VcVersion.V9:
query = "SELECT certificate FROM vpx_ext WHERE ext_id='{}'".format(extension)
output = CommandRunner(PSQL_CLI, '-d', 'VCDB', '-U', 'postgres',
'-c', query, '-t', '-A').run_and_get_output()
pem_cert = TextFilter(output).get_text().strip()
else:
pem_cert = None
result[extension] = (thumbprints[index], pem_cert)
return result
def get_extension_thumbprint(extension):
"""
Get thumbprint for a specific extension
:param extension: extension name
:return: extension thumbprint
"""
query = "SELECT thumbprint FROM vpx_ext WHERE ext_id = '{}'".format(extension)
output = CommandRunner(PSQL_CLI, '-d', 'VCDB', '-U', 'postgres',
'-c', query, '-t').run_and_get_output()
thumbprint = TextFilter(output).head(1).get_text().strip()
return thumbprint
def update_extension_thumbprint(extension, thumbprint, cert_pem=None):
if cert_pem is None:
query = "UPDATE vpx_ext SET thumbprint = '{}' WHERE ext_id = '{}'".format(thumbprint, extension)
else:
one_line_pem = cert_pem.strip().replace('\n', '\\n')
query = "UPDATE vpx_ext SET thumbprint = '{}', certificate = E'{}' WHERE ext_id = '{}'".format(thumbprint, one_line_pem, extension)
ret_code, _, _ = CommandRunner(PSQL_CLI, '-d', 'VCDB', '-U', 'postgres', '-c',
query, '-t').run()
return ret_code == 0
def get_certificate_management_mode():
"""
Get certificate management node
"""
query = 'SELECT value FROM vpx_parameter WHERE name=\'vpxd.certmgmt.mode\''
output = CommandRunner(PSQL_CLI, '-d', 'VCDB', '-U', 'postgres',
'-c', query, '-t').run_and_get_output()
return TextFilter(output).head(1).get_text().strip()
def get_number_of_hosts():
"""
Get the number of hosts
"""
query = 'SELECT COUNT(id) FROM vpx_host WHERE enabled=1'
output = CommandRunner(PSQL_CLI, '-d', 'VCDB', '-U', 'postgres',
'-c', query, '-t').run_and_get_output()
return TextFilter(output).head(1).get_text().strip()
def get_hosts():
"""
Get the hosts
"""
query = 'SELECT id, dns_name, ip_address FROM vpx_host WHERE enabled=1 ORDER BY dns_name ASC, ip_address ASC'
output = CommandRunner(PSQL_CLI, '-d', 'VCDB', '-U', 'postgres', '-c', query, '-t').run_and_get_output()
return TextFilter(output).get_text().strip().splitlines()
def get_host_and_cluster_id():
"""
Get the hosts id and cluster id it belongs too
"""
query = '''SELECT ent.id, ent.parent_id FROM vpx_entity as ent LEFT JOIN vpx_object_type AS obj ON
ent.type_id = obj.id WHERE obj.name = 'HOST' '''
output = CommandRunner(PSQL_CLI, '-d', 'VCDB', '-U', 'postgres',
'-c', query, '-t').run_and_get_output()
return TextFilter(output).get_text().strip().splitlines()
def get_clusters():
query = '''SELECT ent.id, ent.name FROM vpx_entity as ent LEFT JOIN vpx_object_type AS obj
ON ent.type_id = obj.id WHERE obj.name = 'CLUSTER_COMPUTE_RESOURCE' '''
output = CommandRunner(PSQL_CLI, '-d', 'VCDB', '-U', 'postgres',
'-c', query, '-t').run_and_get_output()
return TextFilter(output).get_text().strip().splitlines()
def get_ssl_thumbprint_from_vcdb(access_string):
"""
Get the expected_ssl_thumbprint and host_ssl_thumbprint
"""
query = 'SELECT expected_ssl_thumbprint,host_ssl_thumbprint FROM vpx_host where ' \
'dns_name = \'{0}\' or ip_address = \'{0}\' '.format(access_string)
output = CommandRunner(PSQL_CLI, '-d', 'VCDB', '-U', 'postgres',
'-c', query, '-t').run_and_get_output()
return TextFilter(output).get_text().strip().splitlines()
def get_vmca_configuration_from_vcdb():
"""
Get the vpxd.certmgmt.mode and vpxd.certmgmt.cn.* values from the database
"""
query = "SELECT name,value FROM vpx_parameter WHERE name='vpxd.certmgmt.mode' OR name LIKE 'vpxd.certmgmt.certs.cn.%' ORDER BY name"
output = CommandRunner(PSQL_CLI, '-d', 'VCDB', '-U', 'postgres',
'-c', query, '-t').run_and_get_output()
return TextFilter(output).get_text().strip().splitlines()
def scheduled_task_table_exists():
"""
Determine if the vpx_sched_persistent_user_token table exists
"""
query = "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema='vc' AND table_name='vpx_sched_persistent_user_token')"
output = CommandRunner(PSQL_CLI, '-d', 'VCDB', '-U', 'postgres',
'-c', query, '-t').run_and_get_output()
return TextFilter(output).get_text().strip().lower() == 't'
def get_number_scheduled_tasks():
"""
Get number of user-defined Scheduled Tasks in vCenter
"""
query = "SELECT COUNT(usertoken_id) FROM vpx_sched_persistent_user_token"
output = CommandRunner(PSQL_CLI, '-d', 'VCDB', '-U', 'postgres',
'-c', query, '-t').run_and_get_output()
return TextFilter(output).get_text().strip()
+305
View File
@@ -0,0 +1,305 @@
# Copyright (c) 2024-2025 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import logging
import json
import re
from lib.console import (
print_task, print_task_status, print_header, ColorKey, print_text
)
from lib.environment import Environment
from lib.command_runner import CommandRunner
from lib.host_utils import VcVersion, get_vc_version, is_file_exists
from lib.menu import MenuInput
from lib.text_utils import TextFilter
from lib.exceptions import (OperationFailed, CommandExecutionError)
VECS_CLI = '/usr/lib/vmware-vmafd/bin/vecs-cli'
logger = logging.getLogger(__name__)
def get_all_certificates(store):
"""
Get certificates and aliases from 'vecs-cli entry list' output
:param store: Certificate store
:return: tuple of certificate list and alias list
"""
args = [VECS_CLI, 'entry', 'list', '--store', store]
client = CommandRunner(*args, expected_return_code=0)
return_code, stdout, stderr = client.run()
certs_pem = TextFilter(stdout).match_block(
r'.*\s+-----BEGIN CERTIFICATE-----|.*\s+-----BEGIN X509 CRL-----',
'-----END CERTIFICATE-----|-----END X509 CRL-----', concatenate=True)\
.remove('Certificate :\t').get_lines()
aliases = TextFilter(stdout).start_with('Alias').remove_white_spaces().cut(':', [1]).get_lines()
return certs_pem, aliases
def get_all_ca_certificates():
"""
Get all trusted root CA certificates from VECS
"""
return get_all_certificates('TRUSTED_ROOTS')
def get_certificate_aliases(store):
"""
Get aliases from VECS via 'vecs-cli entry list' command output
:param store: Certificate store
:return: list of aliases
"""
args = [VECS_CLI, 'entry', 'list', '--store', store]
client = CommandRunner(*args, expected_return_code=0)
return_code, stdout, stderr = client.run()
return TextFilter(stdout).start_with('Alias').remove_white_spaces().cut(':', [1]).get_lines()
def get_store_list():
"""
Get store list in VECS via 'vecs-cli store list' command output
:return: list of certificate stores in VECS
"""
args = [VECS_CLI, 'store', 'list']
client = CommandRunner(*args, expected_return_code=0)
return_code, stdout, stderr = client.run()
return stdout.splitlines()
def get_expected_stores_to_check():
storeDB = {
# Store Name Version Constraint (None means all)
# ---------- -----------------------------------
'MACHINE_SSL_CERT': None,
'TRUSTED_ROOTS': None,
'TRUSTED_ROOT_CRLS': None,
'machine': None,
'vsphere-webclient': None,
'vpxd': None,
'vpxd-extension': None,
'SMS': None,
'APPLMGMT_PASSWORD': None,
'data-encipherment': None,
'hvc': None,
'wcp': [VcVersion.V7, VcVersion.V8],
'wcpsvc': [VcVersion.V9],
}
vc_version = get_vc_version()
stores = []
for store, constraint in storeDB.items():
if not constraint or vc_version in constraint:
stores.append(store)
return stores
def get_missing_stores():
current_stores = get_store_list()
expected_stores = get_expected_stores_to_check()
missing_stores = []
for store in expected_stores:
if store not in current_stores:
missing_stores.append(store)
return missing_stores
def get_certificate(store, alias):
"""
Get a specific certificate from VECS via 'vecs-cli entry getcert' command output
:param store: Certificate store in VECS
:param alias: Certificate alias
:return: Certificate in PEM format
"""
args = [VECS_CLI, 'entry', 'getcert', '--store', store, '--alias', alias]
client = CommandRunner(*args, expected_return_code=0)
return_code, stdout, stderr = client.run()
return stdout
def get_key(store, alias):
"""
Get a specific key entry from VECS via 'vecs-cli entry getkey' command output
:param store: Certificate store in VECS
:param alias: certificate alias
:return: key in PEM format
"""
args = [VECS_CLI, 'entry', 'getkey', '--store', store, '--alias', alias]
command = CommandRunner(*args, expected_return_code=0)
return_code, stdout, stderr = command.run()
return stdout
def add_entry(store, alias, cert_file, key_file=None):
"""
Add new entry into VECS
:param store: the store name
:param alias: alias for the new entry
:param cert_file: certificate file (PEM)
:param key_file: key file (PEM)
"""
args = [VECS_CLI, 'entry', 'create', '--store', store, '--alias', alias, '--cert', cert_file]
if key_file:
args.extend(['--key', key_file])
CommandRunner(*args, expected_return_code=0).run()
def delete_entry(store, alias):
"""
Delete certificate/key entry from VECS
:param store: store name
:param alias: certificate/key alias
"""
args = [VECS_CLI, 'entry', 'delete', '--store', store, '--alias', alias, '-y']
client = CommandRunner(*args, expected_return_code=0).run()
def get_current_vecs_store_permissions():
current_vecs_permissions = dict()
stores = get_expected_stores_to_check()
for store in stores:
owner, read_users, write_users = get_vecs_store_permission(store)
current_vecs_permissions[store] = {'owner' : owner,
'read' : read_users,
'write' : write_users}
return current_vecs_permissions
def get_template_vecs_store_permissions(template_file):
desired_vecs_permissions = dict()
if is_file_exists(template_file):
try:
template_file_handler = open(template_file)
template_data = json.load(template_file_handler)
template_file_handler.close()
except Exception as e:
error_message = 'Unable to open template file: {}'.format(str(e))
logger.error(error_message)
raise OperationFailed(error_message)
for store, permissions in template_data.items():
desired_vecs_permissions[store] = dict()
desired_vecs_permissions[store]['owner'] = permissions['owner']
desired_vecs_permissions[store]['read'] = permissions['read']
desired_vecs_permissions[store]['write'] = permissions['write']
return desired_vecs_permissions
def get_vecs_store_permission(store):
"""
Get VECS store permissions
:param store: VECS store
:return: tuple (owner, list of users with read permission, list of users with write permissions)
"""
args = [VECS_CLI, 'store', 'get-permissions', '--name', store]
client = CommandRunner(*args, expected_return_code=0)
_, stdout, _ = client.run()
owner = TextFilter(stdout).start_with('OWNER').cut(':', [1]).get_text().strip()
read_users = TextFilter(stdout).match('.*read$').cut('\t', [0]).get_lines()
write_users = TextFilter(stdout).match('.*write$').cut('\t', [0]).get_lines()
return owner, read_users, write_users
def force_refresh():
"""
Force VECS refresh from VMDir
"""
CommandRunner(VECS_CLI, 'force-refresh', expected_return_code=0).run()
def grant_vecs_permission(store, user, perm):
args = [VECS_CLI, 'store', 'permission', '--name', store, '--user', user, '--grant', perm]
try:
CommandRunner(*args, expected_return_code=0).run()
except CommandExecutionError:
error_message = 'Unable to assign {} permission to user {} on store {}'.format(perm, user, store)
logger.error(error_message)
raise OperationFailed(error_message)
def create_store(store):
"""
Create store in VECS via 'vecs-cli store create' command
"""
args = [VECS_CLI, 'store', 'create', '--name', store]
try:
CommandRunner(*args, expected_return_code=0).run()
except CommandExecutionError:
error_message = "Unable to create VECS store {}".format(store)
logger.error(error_message)
raise OperationFailed(error_message)
def delete_store(store):
"""
Create store in VECS via 'vecs-cli store create' command
"""
args = [VECS_CLI, 'store', 'delete', '--name', store, '-y']
try:
CommandRunner(*args, expected_return_code=0).run()
except CommandExecutionError:
error_message = "Unable to delete VECS store {}".format(store)
logger.error(error_message)
raise OperationFailed(error_message)
def create_vecs_store_template(template_file):
print_header('Create VECS Store Template')
print_task('Creating template file')
template = get_current_vecs_store_permissions()
try:
with open(template_file, 'w') as outfile:
json.dump(template, outfile)
print_task_status('OK')
except Exception as e:
error_message = 'Unable to write template file: {}'.format(str(e))
logger.error(error_message)
raise OperationFailed(error_message)
def manage_missing_vecs_stores(missing_stores):
prompt = MenuInput('Some VECS stores are missing, recreate them? [N]: ', acceptable_inputs=['Y', 'N'], default_input='N')
print()
if prompt.get_input() == 'Y':
print_header('Recreate VECS Store')
for store in missing_stores:
print_task(store)
create_store(store)
print_task_status('OK')
def manage_missing_vecs_permissions(missing_permissions):
prompt = MenuInput('Some VECS stores are missing expected permissions, reassign them? [N]: ', acceptable_inputs=['Y', 'N'], default_input='N')
print()
if prompt.get_input() == 'Y':
print_header('Reassign VECS Store Permissions')
if missing_permissions['read']:
print_text('Reassigning READ permissions:')
for store, missing_read_users in missing_permissions['read'].items():
print_text(' Store {}:'.format(store))
for user in missing_read_users:
print_task(' {}'.format(user))
grant_vecs_permission(store, user, 'read')
print_task_status('OK')
if missing_permissions['write']:
print_text('Reassigning WRITE permissions:')
for store, missing_write_users in missing_permissions['write'].items():
print_text(' Store {}:'.format(store))
for user in missing_write_users:
print_task(' {}'.format(user))
grant_vecs_permission(store, user, 'write')
print_task_status('OK')
+607
View File
@@ -0,0 +1,607 @@
# Copyright (c) 2024-2025 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import ldap3 as ldap
import logging
import re
from lib import certificate_utils as certutil
from lib.environment import Environment
from lib.certificate_utils import build_pem_certificate
from lib.command_runner import CommandRunner
from lib.console import print_task, print_text_error
from lib.host_utils import VcVersion, get_hostname, get_vc_version
from lib.ldap_utils import Ldap, LdapException, get_user_dn
from lib.text_utils import TextFilter
from lib.console import (print_task_status)
from lib.exceptions import OperationFailed, CommandExecutionError
DIR_CLI = '/usr/lib/vmware-vmafd/bin/dir-cli'
VDCADMINTOOL = '/usr/lib/vmware-vmdir/bin/vdcadmintool'
cache_ca_keyids = None
cache_ca_certificate_map = dict()
logger = logging.getLogger(__name__)
def get_ldap_connection(use_machine_account=False):
env = Environment.get_environment()
if use_machine_account:
user_dn = env.get_value('VMDIR_MACHINE_ACCOUNT_DN')
user_password = env.get_value('VMDIR_MACHINE_ACCOUNT_PASSWORD')
else:
sso_username = env.get_value('SSO_USERNAME')
user_password = env.get_value('SSO_PASSWORD')
user_dn = get_user_dn(sso_username)
hostname = get_hostname()
return Ldap.open_ldap_connection(node=hostname, user_dn=user_dn, password=user_password)
def close_ldap_connection(ldap_connection):
Ldap.close_ldap_connection(ldap_connection)
def perform_ldap_search(search_base, search_filter, search_attributes, search_scope=ldap.SUBTREE,
use_machine_account=False, throw_exception=False):
"""
Utility method to perform LDAP search
:param search_base: LDAP search base DN
:param search_filter: LDAP search filter
:param search_attributes: LDAP search attribute
:param search_scope: LDAP search scope (default: ldap.SUBTREE)
:param use_machine_account: Flag to use VMDir machine account instead of SSO user account
:param throw_exception: When LDAP search fails, throw exception instead of empty result
:return: list of matching object, as dict with keys from search_attributes paramerter
"""
results = []
connection = None
try:
connection = get_ldap_connection(use_machine_account)
if not Ldap.ldap_search(connection, search_base, search_filter, search_scope, search_attributes):
logger.error("Unable to perform LDAP search base_dn={}, filter={}" \
.format(search_base, search_filter))
if throw_exception:
raise LdapException(connection.result['result'], connection.result['description'])
else:
return results
if connection.result:
for entry in connection.response:
attributes = entry['attributes']
if 'dn' in search_attributes and not attributes['dn']:
attributes['dn'] = entry['dn'].replace(', cn=', ',cn=')
results.append(attributes)
finally:
if connection:
close_ldap_connection(connection)
return results
def perform_ldap_add(service_dn, object_class, attributes, use_machine_account=False):
"""
A wrapper for LDAP modify operation
"""
connection = None
try:
connection = get_ldap_connection(use_machine_account)
if not Ldap.ldap_add(connection, service_dn, object_class, attributes):
logger.error("Unable to add LDAP entry dn={}, attributes={}".format(service_dn, str(attributes)))
raise LdapException(connection.result['result'], connection.result['description'])
finally:
if connection:
close_ldap_connection(connection)
def perform_ldap_modify(service_dn, attribute, value, operation=ldap.MODIFY_REPLACE,
use_machine_account=False):
"""
A wrapper for LDAP modify operation
"""
connection = None
try:
connection = get_ldap_connection(use_machine_account)
if not Ldap.ldap_modify(connection, service_dn, attribute, operation, value):
logger.error("Unable to modify LDAP entry dn={}, attribute={}".format(service_dn, attribute))
raise LdapException(connection.result['result'], connection.result['description'])
finally:
if connection:
close_ldap_connection(connection)
def perform_ldap_delete(service_dn, use_machine_account=False):
"""
A wrapper for LDAP delete operation
"""
connection = None
try:
connection = get_ldap_connection(use_machine_account)
if not Ldap.ldap_delete(connection, service_dn):
logger.error("Unable to delete LDAP entry dn={}".format(service_dn))
raise LdapException(connection.result['result'], connection.result['description'])
finally:
if connection:
close_ldap_connection(connection)
def get_solution_users():
"""
Get solution users list from VMDir via LDAP
:return: list of solution users
"""
env = Environment.get_environment()
machine_id = env.get_value('MACHINE_ID')
domain_dn = env.get_value('SSO_DOMAIN_DN')
postfix = "-{}".format(machine_id)
search_base = "cn=ServicePrincipals,{}".format(domain_dn)
search_filter = "(&(objectClass=vmwServicePrincipal)(cn=*{}))".format(postfix)
search_attributes = ['cn']
results = perform_ldap_search(search_base, search_filter, search_attributes)
return [entry['cn'].replace(postfix, '') for entry in results]
def get_sts_tenant_certificates(include_tenant_credential=True,
include_certificate_chain=True):
"""
Get STS tenant user certificates
:return: dict(cn: certificate list)
"""
env = Environment.get_environment()
sso_domain = env.get_value('SSO_DOMAIN')
domain_dn = env.get_value('SSO_DOMAIN_DN')
base_dn = "cn={},cn=Tenants,cn=IdentityManager,cn=Services,{}".format(sso_domain, domain_dn)
if include_certificate_chain and include_tenant_credential:
ldap_filter = \
'(|(objectClass=vmwSTSTenantCredential)(&(objectclass=vmwSTSTenantTrustedCertificateChain)(cn=TrustedCertChain*)))'
elif include_tenant_credential:
ldap_filter = '(objectClass=vmwSTSTenantCredential)'
elif include_certificate_chain:
ldap_filter = '(&(objectclass=vmwSTSTenantTrustedCertificateChain)(cn=TrustedCertChain*))'
else:
return None
ldap_attributes = ['cn', 'userCertificate']
results = perform_ldap_search(base_dn, ldap_filter, ldap_attributes)
tenant_certs = dict()
for entry in results:
tenant_certs[entry['cn']] = [build_pem_certificate(cert) for cert in entry['userCertificate']]
return tenant_certs
def get_solution_user_certificate(solution_user):
"""
Get solution user certificate from VMDir via LDAP
:param solution_user: solution user
:return: Solution user certificate in PEM format
"""
env = Environment.get_environment()
machine_id = env.get_value('MACHINE_ID')
domain_dn = env.get_value('SSO_DOMAIN_DN')
base_dn = "cn={}-{},cn=ServicePrincipals,{}".format(solution_user, machine_id, domain_dn)
ldap_filter = '(objectClass=vmwServicePrincipal)'
ldap_attributes = ['userCertificate']
results = perform_ldap_search(base_dn, ldap_filter, ldap_attributes, search_scope=ldap.BASE)
certs = []
for entry in results:
for cert in entry['userCertificate']:
# only expect single certificate
certs.append(build_pem_certificate(cert))
return '\n'.join(certs)
def get_all_ca_subject_keyids(use_cache=False):
"""
Get list of subject keyId of trusted CA certificates via 'dir-cli trustedcert list' output
The result will be cached
:param use_cache: If True, the previous cached result will be used (default: True)
:return: list of subject keyIds of trusted CA certificates
"""
global cache_ca_keyids
if use_cache and cache_ca_keyids is not None:
return cache_ca_keyids
env = Environment.get_environment()
sso_username = env.get_value('SSO_USERNAME')
sso_password = env.get_value('SSO_PASSWORD')
args = [DIR_CLI, 'trustedcert', 'list', '--login', sso_username, '--password', sso_password]
ret, stdout, _ = CommandRunner(*args).run()
cache_ca_keyids = TextFilter(stdout).start_with('CN(id):').cut(':', [1]).remove_white_spaces().get_lines()
return cache_ca_keyids
def get_ca_certificate(subject_keyid, use_cache=True):
"""
Get CA certificate in VMDir via 'dir-cli trustedcert get' command output
:param subject_keyid: the certificate' subject keyId
:param use_cache: if True, the previous cached result will be used instead
:return: trusted CA certificate in PEM format
"""
global cache_ca_certificate_map
if not subject_keyid:
return None
if use_cache:
cert_cache = cache_ca_certificate_map.get(subject_keyid)
if cert_cache:
return cert_cache
env = Environment.get_environment()
sso_username = env.get_value('SSO_USERNAME')
sso_password = env.get_value('SSO_PASSWORD')
args = [DIR_CLI, 'trustedcert', 'get', '--login', sso_username, '--password', sso_password,
'--id', subject_keyid, '--outcert', '/dev/stdout']
ret, stdout, _ = CommandRunner(*args).run()
lines = TextFilter(stdout).match_block('-----BEGIN CERTIFICATE-----',
'-----END CERTIFICATE-----').get_lines()
pem_cert = '\n'.join(lines)
cache_ca_certificate_map[subject_keyid] = pem_cert
return pem_cert
def get_all_ca_certificates():
"""
Get all trusted CA certificates in VMDir via dir-cli command)
:return: list of CA certificates in PEM format
"""
subject_keyids = get_all_ca_subject_keyids(False)
certs = []
for subject_keyid in subject_keyids:
pem_cert = get_ca_certificate(subject_keyid)
certs.append(pem_cert)
return certs, subject_keyids
def get_service_principals():
"""
Get service principal list from VMDir via dir-cli
:return: list of service principals
"""
env = Environment.get_environment()
sso_username = env.get_value('SSO_USERNAME')
sso_password = env.get_value('SSO_PASSWORD')
args = [DIR_CLI, 'service', 'list', '--login', sso_username, '--password', sso_password]
ret, stdout, _ = CommandRunner(*args).run()
return TextFilter(stdout).cut(fields=[1]).get_lines()
def init_env_identity_source():
env = Environment.get_environment()
if env.get_map().get('SSO_USERNAME') is None or \
env.get_value('EXTERNAL_IDENTITY_SOURCE_CONFIGURED') is not None:
return
logger.info('Checking identity source settings')
identity_sources = get_identity_sources()
source_types = [item['type'] for item in identity_sources]
logger.info("Identity sources: {}".format(source_types))
env.set_value('EXTERNAL_IDENTITY_SOURCE_CONFIGURED', len(source_types) > 0)
def get_identity_sources(use_machine_account=False):
"""
Get the identity sources settings
return: list of configured identity source represented in the following dict object:
{
"type": one of ('AD over LDAP', 'ADFS', 'OpenLDAP')
"domain_name": domain name
"certificates": CA certificate for this identity source
}
"""
logger.info('Obtaining identity source settings')
env = Environment.get_environment()
sso_domain = env.get_value('SSO_DOMAIN')
domain_dn = env.get_value('SSO_DOMAIN_DN')
search_list = [
('AD over LDAP', 'IdentityProviders', 'IDENTITY_STORE_TYPE_LDAP_WITH_AD_MAPPING'),
('ADFS', 'VCIdentityProviders', 'IDENTITY_STORE_TYPE_LDAP_WITH_AD_MAPPING'),
('OpenLDAP', 'IdentityProviders', 'IDENTITY_STORE_TYPE_LDAP')
]
identity_sources = []
connection = None
try:
connection = get_ldap_connection(use_machine_account=use_machine_account)
ldap_attributes = ['vmwSTSDomainName', 'userCertificate']
for type_name, provider_cn, provider_type in search_list:
base_dn = "cn={},cn={},cn=Tenants,cn=IdentityManager,cn=Services,{}"\
.format(provider_cn, sso_domain, domain_dn)
ldap_filter = "(vmwSTSProviderType={})".format(provider_type)
if not Ldap.ldap_search(connection, base_dn, ldap_filter, ldap.SUBTREE, ldap_attributes):
# this search may return noSuchObject due to an invalid base_dn
if connection.result['result'] == 32:
continue
logger.error('Unable to perform LDAP search for the identity source setting')
raise LdapException(connection.result['result'], connection.result['description'])
if not connection.result:
continue
for entry in connection.response:
attributes = entry['attributes']
result = dict()
result['type'] = type_name
result['domain_name'] = attributes['vmwSTSDomainName']
certs = []
for cert in attributes['userCertificate']:
# only expect single certificate
certs.append(build_pem_certificate(cert))
result['certificates'] = certs
identity_sources.append(result)
logger.info("Identity source: type={}, domain={}".format(type_name, result['domain_name']))
finally:
if connection:
close_ldap_connection(connection)
return identity_sources
def get_sso_domain_nodes():
logger.info('Obtaining SSO domain nodes')
domain_dn = Environment.get_environment().get_value('SSO_DOMAIN_DN')
base_dn_list = ["ou=Domain Controllers,{}".format(domain_dn), "ou=Computers,{}".format(domain_dn)]
ldap_filter = '(objectClass=computer)'
ldap_attributes = ['cn']
nodes = []
for base_dn in base_dn_list:
results = perform_ldap_search(base_dn, ldap_filter, ldap_attributes)
for entry in results:
node = entry['cn']
if node not in nodes:
nodes.append(node)
logger.info("Found node: {}".format(node))
return nodes
def get_all_lookup_service_endpoints(search_base=None):
"""
Get all endpoint entries
:return:
"""
logger.info('Obtaining all endpoints')
domain_dn = Environment.get_environment().get_value('SSO_DOMAIN_DN')
if search_base is None:
search_base = "cn=Sites,cn=Configuration,{}".format(domain_dn)
search_filter = '(|(objectclass=vmwLKUPServiceEndpoint)(objectClass=vmwLKUPEndpointRegistration))'
search_attributes = ['dn', 'objectClass', 'vmwLKUPURI', 'vmwLKUPEndpointSslTrust', 'vmwLKUPSslTrustAnchor']
return perform_ldap_search(search_base, search_filter, search_attributes)
def get_node_trust_anchors(fqdn_or_ip):
"""
Get SSL trust anchors by filtering the endpoint URI using {fqdn_or_ip}
:param fqdn_or_ip:
:return:
"""
logger.info("Obtaining node trust anchors: fqdn_or_ip: {}".format(fqdn_or_ip))
trust_anchors = []
pattern = "^https://{0}:.*|^https://{0}/.*".format(fqdn_or_ip)
endpoints = get_all_lookup_service_endpoints()
for endpoint in endpoints:
if not re.match(pattern, endpoint['vmwLKUPURI']):
continue
for cert in sum([endpoint['vmwLKUPEndpointSslTrust'], endpoint['vmwLKUPSslTrustAnchor']], []):
pem_cert = certutil.build_pem_certificate(cert)
if pem_cert and pem_cert not in trust_anchors:
trust_anchors.append(pem_cert)
logger.info('Trust anchors found for {}: {}'.format(fqdn_or_ip, trust_anchors))
return trust_anchors
def get_endpoint_service_type(service_id):
domain_dn = Environment.get_environment().get_value('SSO_DOMAIN_DN')
search_base = "cn=Sites,cn=Configuration,{}".format(domain_dn)
search_filter = "(cn={})".format(service_id)
search_attributes = ['vmwLKUPType']
result = perform_ldap_search(search_base, search_filter, search_attributes)
if result:
return result[0]['vmwLKUPType']
search_filter = '(objectClass=vmwLKUPService)'
search_attributes = ['dn', 'vmwLKUPServiceType']
result = perform_ldap_search(search_base, search_filter, search_attributes)
for entry in result:
if service_id in entry['dn']:
# remove prefix 'urn:'
return entry['vmwLKUPServiceType'][4:]
return ''
def get_registered_vcenters():
"""
Get registered VCenters
:return: list of tuple (deployment_node_id, node_dn)
"""
logger.info('Obtaining list of registered vCenters')
domain_dn = Environment.get_environment().get_value('SSO_DOMAIN_DN')
base_dn = "cn=Sites,cn=Configuration,{}".format(domain_dn)
ldap_filter = '(vmwLKUPType=vcenterserver)'
ldap_attributes = ['vmwLKUPDeploymentNodeId', 'dn']
results = perform_ldap_search(base_dn, ldap_filter, ldap_attributes)
return [(entry['vmwLKUPDeploymentNodeId'], entry['dn']) for entry in results]
def get_endpoint_registrations(base_dn):
ldap_filter = '(objectClass=vmwLKUPEndpointRegistration)'
ldap_attributes = ['vmwLKUPURI']
results = perform_ldap_search(base_dn, ldap_filter, ldap_attributes)
return [entry['vmwLKUPURI'] for entry in results]
def init_env_cac():
env = Environment.get_environment()
# if env.get_map().get('SSO_USERNAME') is None or \
# env.get_value('CAC_CONFIGURED') is not None:
# return
if env.get_value('CAC_CONFIGURED') is not None:
return
vc_version = get_vc_version()
if vc_version >= VcVersion.V9:
logger.info('SmartCards are not supported since version {}'.format(VcVersion.V9.value))
env.set_value('CAC_CONFIGURED', False)
return
logger.info('Checking SmartCard Authentication settings')
sso_domain = env.get_value('SSO_DOMAIN')
domain_dn = env.get_value('SSO_DOMAIN_DN')
search_base = "cn={},cn=Tenants,cn=IdentityManager,cn=Services,{}".format(sso_domain, domain_dn)
search_filter = '(objectclass=vmwSTSTenant)'
search_attributes = ['vmwSTSAuthnTypes']
results = perform_ldap_search(search_base, search_filter, search_attributes, use_machine_account=True)
is_cac_configured = False
logger.info('LDAP search results: {}'.format(results))
for entry in results:
if entry['vmwSTSAuthnTypes'] == 4 or 4 in entry['vmwSTSAuthnTypes']:
is_cac_configured = True
env.set_value('CAC_CONFIGURED', is_cac_configured)
def get_smart_card_issuing_ca_certs():
logger.info('Obtaining SmartCard issuing CA certificates')
env = Environment.get_environment()
sso_domain = env.get_value('SSO_DOMAIN')
domain_dn = env.get_value('SSO_DOMAIN_DN')
search_base = \
"cn=DefaultClientCertCAStore,cn=ClientCertAuthnTrustedCAs,cn=Default,cn=ClientCertificatePolicies,"\
"cn={},cn=Tenants,cn=IdentityManager,cn=Services,{}".format(sso_domain, domain_dn)
search_filter = '(objectClass=vmwSTSTenantTrustedCertificateChain)'
search_attributes = ['userCertificate']
result = perform_ldap_search(search_base, search_filter, search_attributes, use_machine_account=True)
certs = []
for entry in result:
certs.extend(build_pem_certificate(cert) for cert in entry['userCertificate'])
return certs
def get_all_sso_sites():
domain_dn = Environment.get_environment().get_value('SSO_DOMAIN_DN')
search_base = "cn=Sites,cn=Configuration,{}".format(domain_dn)
search_filter = '(objectClass=*)'
search_attributes = ['cn']
result = perform_ldap_search(search_base, search_filter, search_attributes, search_scope=ldap.LEVEL)
return [entry['cn'] for entry in result]
def unpublish_trusted_certificate(cert_file):
"""
Unpublish certificate in VMDir
:param cert_file: Certificate file to unpublish
"""
env = Environment.get_environment()
sso_username = env.get_value('SSO_USERNAME')
sso_password = env.get_value('SSO_PASSWORD')
CommandRunner(DIR_CLI, 'trustedcert', 'unpublish', '--login', sso_username, '--password',
sso_password, '--cert', cert_file, expected_return_code=0).run()
def publish_trusted_certificate(cert_file, is_chain=False):
"""
Publish certificate in VMDir
:param cert_file: Certificate file to unpublish
:param is_chain: Publish all certificate in chain
"""
env = Environment.get_environment()
sso_username = env.get_value('SSO_USERNAME')
sso_password = env.get_value('SSO_PASSWORD')
args = [DIR_CLI, 'trustedcert', 'publish', '--login', sso_username, '--password', sso_password,
'--cert', cert_file]
if is_chain:
args.append('--chain')
CommandRunner(*args, expected_return_code=0).run()
def remove_ca_certificate_from_ldap(subject_keyid):
domain_dn = Environment.get_environment().get_value('SSO_DOMAIN_DN')
cert_dn = "cn={},cn=Certificate-Authorities,cn=Configuration,{}".format(subject_keyid, domain_dn)
perform_ldap_delete(cert_dn)
def get_sddc_manager():
# SDDC_MANAGER=$($LDAP_SEARCH -LLL -h $PSC_LOCATION -b
# "cn=$SSO_DOMAIN,cn=Tenants,cn=IdentityManager,cn=Services,$VMDIR_DOMAIN_DN"
# -D "$VMDIR_MACHINE_ACCOUNT_DN" -y $STAGE_DIR/.machine-account-password '(objectclass=vmwSTSTenant)'
# vmwSTSLogonBanner | tr -d '\n' | awk -F'::' '{print $NF}' | tr -d ' ' | base64 -d 2>/dev/null | grep 'SDDC Manager' | awk -F '[()]' '{print $2}' | grep -v '^$')
env = Environment.get_environment()
sso_domain = env.get_value('SSO_DOMAIN')
domain_dn = env.get_value('SSO_DOMAIN_DN')
search_base = "cn={},cn=Tenants,cn=IdentityManager,cn=Services,{}".format(sso_domain, domain_dn)
search_filter = '(objectClass=vmwSTSTenant)'
search_attributes = ['vmwSTSLogonBanner']
result = perform_ldap_search(search_base, search_filter, search_attributes, search_scope=ldap.LEVEL)
return [entry['cn'] for entry in result]
def update_solution_user_certificate_in_vmdir(soluser, cert_file):
env = Environment.get_environment()
sso_username = env.get_value('SSO_USERNAME')
sso_password = env.get_value('SSO_PASSWORD')
machine_id = env.get_value('MACHINE_ID')
args = [DIR_CLI, 'service', 'update', '--name', f"{soluser}-{machine_id}", '--cert',
cert_file, '--login', sso_username, '--password', sso_password]
try:
CommandRunner(*args, expected_return_code=0).run_and_get_output()
except CommandExecutionError:
error_message = f"Unable to update {soluser}-{machine_id} solution user certificate in VMDir"
logger.error(error_message)
raise OperationFailed(error_message)
# ------------------------------
# Replace a Solution User certificate in VMDir
# ------------------------------
def replace_service_principal_certificates(soluser, cert_file):
print_task(f" {soluser}")
update_solution_user_certificate_in_vmdir(soluser, cert_file)
print_task_status("OK")
def verify_service_principals():
print_task('Verifying Service Principal entries exist')
service_principals = get_service_principals()
if service_principals:
env = Environment.get_environment()
machine_id = env.get_value('MACHINE_ID')
solution_users = env.get_value('SOLUTION_USERS')
missing_service_principals = []
for solution_user in solution_users:
service_principal = "{}-{}".format(solution_user, machine_id)
if service_principal not in service_principals:
missing_service_principals.append(service_principal)
if missing_service_principals:
print_text_error('ERROR')
print_text_error("\n------------------------!!! Attention !!!------------------------ ")
print_text_error('The following Service Principal entries are missing:')
for sp in missing_service_principals:
print_text_error(" - {}".format(sp))
print_text_error('\nPlease refer to the following Knowledge Base article')
print_text_error('on using the lsdoctor utility to recreate the missing')
print_text_error('Solution User/Service Principal entries:')
print_text_error('https://knowledge.broadcom.com/external/article/320837/using-the-lsdoctor-tool.html')
else:
print_task_status("OK")
else:
print_text_error('Could not get list of Service Principal entries from VMware Directory')
def get_vmdir_state():
state_output = CommandRunner(VDCADMINTOOL, command_input='6', expected_return_code=0).run_and_get_output()
logger.info('VMware Directory state: {}'.format(state_output))
service_state = TextFilter(state_output).contain('VmDir State').cut('-', [1]).get_text().strip()
return service_state