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 <cert_short_name> - 'localities' - default now is <region> - '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 <Marcelo.DeCastroLoebens@windriver.com>
This commit is contained in:
parent
8ea80c4b27
commit
25a22de9bc
@ -1,20 +1,32 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/python3
|
||||||
# Copyright (c) 2023 Wind River Systems, Inc.
|
# Copyright (c) 2023-2024 Wind River Systems, Inc.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
#
|
#
|
||||||
# This script creates required platform certificates for DX systems.
|
# This script creates/updates required platform certificates during upgrade.
|
||||||
# SX systems leverage the execution ansible upgrade playbook for this.
|
# - 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 subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import yaml
|
||||||
from controllerconfig.common import log
|
from controllerconfig.common import log
|
||||||
|
from time import sleep
|
||||||
|
import os
|
||||||
|
|
||||||
LOG = log.get_logger(__name__)
|
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():
|
def get_system_mode():
|
||||||
# get system_mode from platform.conf
|
|
||||||
lines = [line.rstrip('\n') for line in
|
lines = [line.rstrip('\n') for line in
|
||||||
open('/etc/platform/platform.conf')]
|
open('/etc/platform/platform.conf')]
|
||||||
for line in lines:
|
for line in lines:
|
||||||
@ -24,6 +36,42 @@ def get_system_mode():
|
|||||||
return None
|
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):
|
def create_platform_certificates(to_release):
|
||||||
"""Run ansible playbook to create platform certificates
|
"""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'
|
upgrade_script = 'create-platform-certificates-in-upgrade.yml'
|
||||||
cmd = 'ansible-playbook {}/{} -e "software_version={}"'.format(
|
cmd = 'ansible-playbook {}/{} -e "software_version={}"'.format(
|
||||||
playbooks_root, upgrade_script, to_release)
|
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,
|
sub = subprocess.Popen(cmd, shell=True,
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
stdout, stderr = sub.communicate()
|
stdout, stderr = sub.communicate()
|
||||||
|
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:
|
if sub.returncode != 0:
|
||||||
LOG.error('Command failed:\n %s\n. %s\n%s' % (cmd, stdout, stderr))
|
LOG.error('Command failed:\n %s\n. %s\n%s\n' % (
|
||||||
raise Exception('Cannot create platform certificates.')
|
apply_cmd, stdout.decode('utf-8'), stderr.decode('utf-8')))
|
||||||
LOG.info('Successfully created platform certificates.')
|
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():
|
def main():
|
||||||
@ -67,14 +264,29 @@ def main():
|
|||||||
"action = %s"
|
"action = %s"
|
||||||
% (sys.argv[0], from_release, to_release, action))
|
% (sys.argv[0], from_release, to_release, action))
|
||||||
|
|
||||||
|
for retry in range(0, RETRIES):
|
||||||
|
try:
|
||||||
|
reconfigure_certificates_subject()
|
||||||
mode = get_system_mode()
|
mode = get_system_mode()
|
||||||
|
# For (legacy) SX upgrade, the role that creates the required
|
||||||
if mode == 'simplex':
|
# platform certificates is already executed by the upgrade
|
||||||
LOG.info("%s: System mode is %s. No actions required."
|
# playbook.
|
||||||
% (sys.argv[0], mode))
|
if mode != 'simplex':
|
||||||
return 0
|
|
||||||
|
|
||||||
create_platform_certificates(to_release)
|
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__":
|
if __name__ == "__main__":
|
||||||
|
Loading…
Reference in New Issue
Block a user