From 25a22de9bc9a7e60f022ae3e43a900fb00fa8b78 Mon Sep 17 00:00:00 2001 From: Marcelo Loebens Date: Mon, 8 Apr 2024 17:03:18 -0400 Subject: [PATCH] Fix platform certificate subject during upgrades A parent change has modified the default subject for the platform certificates in 'starlingx/ansible-playbooks'. This change extends the upgrade script '81-create-required-platform-certs.py' to apply the same changes during upgrades. The leaf certificate's following fields will be modified if not customized by the user: - 'commonName' - default now is - 'localities' - default now is - 'organization' - default now is 'starlingx' Test plan: PASS: Manually execute the upgrade script and check the subject fields: - With old default values included in commonName and localities. (should be replaced w/ the new default) - With commonName or localities different from previous defaults. (should be kept the same) Story: 2009811 Task: 49832 Change-Id: If32172419836a02625144a87934fe75802311712 Signed-off-by: Marcelo Loebens --- .../81-create-required-platform-certs.py | 246 ++++++++++++++++-- 1 file changed, 229 insertions(+), 17 deletions(-) diff --git a/controllerconfig/controllerconfig/upgrade-scripts/81-create-required-platform-certs.py b/controllerconfig/controllerconfig/upgrade-scripts/81-create-required-platform-certs.py index 619f767100..664c96ecfe 100644 --- a/controllerconfig/controllerconfig/upgrade-scripts/81-create-required-platform-certs.py +++ b/controllerconfig/controllerconfig/upgrade-scripts/81-create-required-platform-certs.py @@ -1,20 +1,32 @@ -#!/usr/bin/python -# Copyright (c) 2023 Wind River Systems, Inc. +#!/usr/bin/python3 +# Copyright (c) 2023-2024 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # -# This script creates required platform certificates for DX systems. -# SX systems leverage the execution ansible upgrade playbook for this. +# This script creates/updates required platform certificates during upgrade. +# - Certificates are created using ansible playbooks. +# - (Legacy) SX upgrade is already covered by upgrade playbook. # +# - Subject is updated to match new defaults, if not otherwise customized by +# the user: +# - 'commonName' - default now is +# - 'localities' - default now is +# - 'organization' - default now is 'starlingx' import subprocess import sys +import yaml from controllerconfig.common import log +from time import sleep +import os + LOG = log.get_logger(__name__) +KUBE_CMD = 'kubectl --kubeconfig=/etc/kubernetes/admin.conf ' +TMP_FILENAME = '/tmp/update_cert.yml' +RETRIES = 3 def get_system_mode(): - # get system_mode from platform.conf lines = [line.rstrip('\n') for line in open('/etc/platform/platform.conf')] for line in lines: @@ -24,6 +36,42 @@ def get_system_mode(): return None +def get_distributed_cloud_role(): + lines = [line.rstrip('\n') for line in + open('/etc/platform/platform.conf')] + for line in lines: + values = line.split('=') + if values[0] == 'distributed_cloud_role': + return values[1] + return None + + +def get_region_name(): + """Get region name + """ + for line in open('/etc/platform/openrc'): + if 'export ' in line: + values = line.rstrip('\n').lstrip('export ').split('=') + if values[0] == 'OS_REGION_NAME': + return values[1] + return None + + +def get_oam_ip(): + cmd = 'source /etc/platform/openrc && ' \ + '(system addrpool-list --nowrap | awk \'$4 == "oam" { print $14 }\')' + + sub = subprocess.Popen( + cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = sub.communicate() + if sub.returncode == 0: + return stdout.decode('utf-8').rstrip('\n') + else: + LOG.error('Command failed:\n %s\n. %s\n%s\n' + % (cmd, stdout.decode('utf-8'), stderr.decode('utf-8'))) + raise Exception('Cannot retrieve OAM IP.') + + def create_platform_certificates(to_release): """Run ansible playbook to create platform certificates """ @@ -31,13 +79,162 @@ def create_platform_certificates(to_release): upgrade_script = 'create-platform-certificates-in-upgrade.yml' cmd = 'ansible-playbook {}/{} -e "software_version={}"'.format( playbooks_root, upgrade_script, to_release) + + sub = subprocess.Popen( + cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = sub.communicate() + if sub.returncode != 0: + LOG.error('Command failed:\n %s\n. %s\n%s\n' + % (cmd, stdout.decode('utf-8'), stderr.decode('utf-8'))) + raise Exception('Cannot create platform certificates.') + LOG.info('Successfully created platform certificates. Output:\n%s\n' + % stdout.decode('utf-8')) + + +def certificate_exists(certificate, namespace='deployment'): + """Check if certificate exists + """ + cmd = (KUBE_CMD + 'get certificates -n ' + namespace + + ' -o custom-columns=NAME:metadata.name --no-headers') + sub = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = sub.communicate() - if sub.returncode != 0: - LOG.error('Command failed:\n %s\n. %s\n%s' % (cmd, stdout, stderr)) - raise Exception('Cannot create platform certificates.') - LOG.info('Successfully created platform certificates.') + if sub.returncode == 0: + return certificate in stdout.decode('utf-8').splitlines() + else: + LOG.error('Command failed:\n %s\n. %s\n%s\n' + % (cmd, stdout.decode('utf-8'), stderr.decode('utf-8'))) + raise Exception('Cannot retrieve existent certificates ' + 'from namespace: %s.' % namespace) + + +def retrieve_certificate(certificate, namespace='deployment'): + """Retrieve certificate (as YAML text) + """ + get_cmd = (KUBE_CMD + 'get certificate ' + certificate + ' -n ' + + namespace + ' -o yaml') + + sub = subprocess.Popen( + get_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = sub.communicate() + if sub.returncode == 0: + return stdout.decode('utf-8') + else: + LOG.error('Command failed:\n %s\n. %s\n%s\n' + % (get_cmd, stdout.decode('utf-8'), stderr.decode('utf-8'))) + raise Exception('Cannot dump Certificate %s from namespace: %s.' + % (certificate, namespace)) + + +def get_old_default_CN_by_cert(certificate): + """Return the old default CN per certificate + """ + oam_ip = get_oam_ip() + default_CN_by_cert = { + 'system-restapi-gui-certificate': oam_ip, + 'system-registry-local-certificate': oam_ip, + 'system-openldap-local-certificate': 'system-openldap' + } + return default_CN_by_cert[certificate] + + +def update_certificate(certificate, short_name): + """Update the desired subject fields for the certificates + """ + LOG.info("Verifying subject of certificate: %s" % certificate) + loaded_data = yaml.safe_load(retrieve_certificate(certificate)) + + if loaded_data.get('spec', None) is None: + error = ('Certificate %s data is incorrect, missing \'spec\' field.' + % certificate) + LOG.error(error) + raise Exception(error) + + region = get_region_name() + cert_changes = False + same_CN = False + + common_name = loaded_data['spec'].get('commonName', None) + if common_name == get_old_default_CN_by_cert(certificate): + same_CN = True + if certificate != 'system-openldap-local-certificate': + common_name = short_name + loaded_data['spec'].update({'commonName': common_name}) + cert_changes = True + + if same_CN and (loaded_data['spec'].get('subject', None) is None): + loaded_data['spec'].update({ + 'subject': {'localities': [region.lower()], + 'organizations': ['starlingx']}}) + cert_changes = True + else: + # If localities exists, it should have two entries: + # 1) 'subject_L' override + # 2) :: + # We will remove the 2nd to match the new configuration. + localities = \ + loaded_data['spec'].get('subject', {}).get('localities', None) + if localities: + if len(localities) != 2: + LOG.warning('Unexpected number of \'L\' entries in subject ' + 'of certificate %s: %s' + % (certificate, len(localities))) + + unwanted_index = None + for index, item in enumerate(localities): + if (region.lower() + ':' + short_name) in item: + unwanted_index = index + break + + if unwanted_index is not None: + if len(localities) == 1: + localities[0] = region.lower() + else: + localities.pop(unwanted_index) + loaded_data['spec']['subject'].update( + {'localities': localities}) + cert_changes = True + else: + LOG.warning('Expected subject \'L\' entry that identifies ' + 'the certificate not found for %s.' % certificate) + + if cert_changes: + with open(TMP_FILENAME, 'w') as yaml_file: + yaml.safe_dump(loaded_data, yaml_file, default_flow_style=False) + + apply_cmd = KUBE_CMD + 'apply -f ' + TMP_FILENAME + + sub = subprocess.Popen(apply_cmd, shell=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = sub.communicate() + if sub.returncode != 0: + LOG.error('Command failed:\n %s\n. %s\n%s\n' % ( + apply_cmd, stdout.decode('utf-8'), stderr.decode('utf-8'))) + raise Exception('Cannot apply change to certificate %s.' + % certificate) + else: + os.remove(TMP_FILENAME) + LOG.info('Updated subject entries for certificate: %s. ' + 'Output:\n%s\n' % (certificate, stdout.decode('utf-8'))) + + +def reconfigure_certificates_subject(): + """Reconfigure the subject for all desired certs + """ + certificate_short_name = { + 'system-restapi-gui-certificate': 'system-restapi-gui', + 'system-registry-local-certificate': 'system-registry-local', + 'system-openldap-local-certificate': 'system-openldap', + } + + cloud_role = get_distributed_cloud_role() + for cert in certificate_short_name.keys(): + if (cert == 'system-openldap-local-certificate' and + cloud_role == 'subcloud'): + continue + if certificate_exists(cert): + update_certificate(cert, certificate_short_name[cert]) def main(): @@ -67,14 +264,29 @@ def main(): "action = %s" % (sys.argv[0], from_release, to_release, action)) - mode = get_system_mode() - - if mode == 'simplex': - LOG.info("%s: System mode is %s. No actions required." - % (sys.argv[0], mode)) - return 0 - - create_platform_certificates(to_release) + for retry in range(0, RETRIES): + try: + reconfigure_certificates_subject() + mode = get_system_mode() + # For (legacy) SX upgrade, the role that creates the required + # platform certificates is already executed by the upgrade + # playbook. + if mode != 'simplex': + create_platform_certificates(to_release) + LOG.info("Successfully created/updated required platform " + "certificates.") + except Exception as e: + if retry == RETRIES - 1: + LOG.error("Error updating required platform certificates. " + "Please verify logs.") + return 1 + else: + LOG.exception(e) + LOG.error("Exception ocurred during script execution, " + "retrying after 5 seconds.") + sleep(5) + else: + return 0 if __name__ == "__main__":