Merge "Fix platform certificate subject during upgrades"

This commit is contained in:
Zuul 2024-04-23 13:46:51 +00:00 committed by Gerrit Code Review
commit 4356bb7ec4

View File

@ -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 <cert_short_name>
# - 'localities' - default now is <region>
# - '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) <subject_prefix>:<region_name>:<cert_short_name>
# 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__":