add vmware-tools
This commit is contained in:
@@ -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
|
||||
@@ -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('"', '\\"'))
|
||||
@@ -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()
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ''
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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')
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user