rootca update upload cert API
- New REST API for kube-rootca-update-upload-cert - Utility functions to help implementation for this new API, extract information from PEM file that will be received. - Utility function to create custom kubernetes resource (in this case, to create an issuer that will sign certificates with our new rootCA) - Utility function to extract current k8s rootCA on /etc/kubernetes/pki/ca.crt. The procedure will use it to store information in DB and to allow users to follow the change on the respective k8s rootCA - Implementation of upload-cert in sysinv-conductor - Renaming of some states to be presented during the execution of kube-rootca-update procedure - Tox unit tests for ConductorManager additions Story: 2008675 Task: 42406 Change-Id: I0a4dd47ca05ae2ca7f150d813c3ff272d7fa8068 Depends-On: https://review.opendev.org/c/starlingx/config/+/788947 Signed-off-by: Joao Soubihe <JoaoPaulo.Soubihe@windriver.com>
This commit is contained in:
parent
45621b7019
commit
af7e468ee3
|
@ -14,6 +14,7 @@ import wsmeext.pecan as wsme_pecan
|
|||
from fm_api import fm_api
|
||||
from fm_api import constants as fm_constants
|
||||
from oslo_log import log
|
||||
from pecan import expose
|
||||
from pecan import rest
|
||||
from sysinv import objects
|
||||
from sysinv.api.controllers.v1 import base
|
||||
|
@ -28,7 +29,37 @@ from wsme import types as wtypes
|
|||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
LOCK_NAME = 'KubeRootCAUpdateController'
|
||||
LOCK_KUBE_ROOTCA_UPLOAD_CONTROLLER = 'KubeRootCAUploadController'
|
||||
LOCK_KUBE_ROOTCA_UPDATE_CONTROLLER = 'KubeRootCAUpdateController'
|
||||
|
||||
|
||||
class KubeRootCAUploadController(rest.RestController):
|
||||
|
||||
@cutils.synchronized(LOCK_KUBE_ROOTCA_UPLOAD_CONTROLLER)
|
||||
@expose('json')
|
||||
def post(self):
|
||||
fileitem = pecan.request.POST['file']
|
||||
|
||||
if not fileitem.filename:
|
||||
raise wsme.exc.ClientSideError(("Error: No file uploaded"))
|
||||
|
||||
try:
|
||||
fileitem.file.seek(0, os.SEEK_SET)
|
||||
pem_contents = fileitem.file.read()
|
||||
except Exception:
|
||||
return dict(
|
||||
success="",
|
||||
error=("No kube rootca certificate have been added, invalid PEM document"))
|
||||
|
||||
try:
|
||||
output = pecan.request.rpcapi.save_kubernetes_rootca_cert(
|
||||
pecan.request.context,
|
||||
pem_contents
|
||||
)
|
||||
except Exception:
|
||||
msg = "Conductor call for new kube rootca upload failed"
|
||||
return dict(success="", error=msg)
|
||||
return output
|
||||
|
||||
|
||||
class KubeRootCAUpdate(base.APIBase):
|
||||
|
@ -84,10 +115,12 @@ class KubeRootCAUpdate(base.APIBase):
|
|||
class KubeRootCAUpdateController(rest.RestController):
|
||||
"""REST controller for kubernetes rootCA updates."""
|
||||
|
||||
upload = KubeRootCAUploadController()
|
||||
|
||||
def __init__(self):
|
||||
self.fm_api = fm_api.FaultAPIs()
|
||||
|
||||
@cutils.synchronized(LOCK_NAME)
|
||||
@cutils.synchronized(LOCK_KUBE_ROOTCA_UPDATE_CONTROLLER)
|
||||
@wsme_pecan.wsexpose(KubeRootCAUpdate, body=six.text_type)
|
||||
def post(self, body):
|
||||
"""Create a new Kubernetes RootCA Update and start update."""
|
||||
|
@ -138,7 +171,9 @@ class KubeRootCAUpdateController(rest.RestController):
|
|||
"System is not in a valid state for kubernetes rootca update. "
|
||||
"Run system health-query for more details."))
|
||||
|
||||
create_obj = {'state': kubernetes.KUBE_ROOTCA_UPDATE_STARTED}
|
||||
create_obj = {'state': kubernetes.KUBE_ROOTCA_UPDATE_STARTED,
|
||||
'from_rootca_cert': body.get('from_rootca_cert')
|
||||
}
|
||||
new_update = pecan.request.dbapi.kube_rootca_update_create(create_obj)
|
||||
|
||||
entity_instance_id = "%s=%s" % (fm_constants.FM_ENTITY_TYPE_HOST,
|
||||
|
@ -158,7 +193,6 @@ class KubeRootCAUpdateController(rest.RestController):
|
|||
service_affecting=False)
|
||||
self.fm_api.set_fault(fault)
|
||||
LOG.info("Started kubernetes rootca update")
|
||||
|
||||
return KubeRootCAUpdate.convert_with_links(new_update)
|
||||
|
||||
@wsme_pecan.wsexpose(KubeRootCAUpdate, types.uuid)
|
||||
|
|
|
@ -1898,3 +1898,17 @@ CERT_MODE_TO_SECRET_NAME = {
|
|||
SB_SUPPORTED_NETWORKS = {
|
||||
SB_TYPE_CEPH: [NETWORK_TYPE_MGMT, NETWORK_TYPE_CLUSTER_HOST]
|
||||
}
|
||||
|
||||
BEGIN_CERTIFICATE_MARKER = b"-----BEGIN CERTIFICATE-----\n"
|
||||
END_CERTIFICATE_MARKER = b"\n-----END CERTIFICATE-----\n"
|
||||
BEGIN_PRIVATE_KEY_MARKER = b"-----BEGIN PRIVATE KEY-----\n"
|
||||
END_PRIVATE_KEY_MARKER = b"\n-----END PRIVATE KEY-----\n"
|
||||
|
||||
# Kubernetes root CA certficate update phases
|
||||
KUBE_CERT_UPDATE_TRUSTBOTHCAS = "trustbothcas"
|
||||
KUBE_CERT_UPDATE_UPDATECERTS = "updatecerts"
|
||||
KUBE_CERT_UPDATE_TRUSTNEWCA = "trustnewca"
|
||||
|
||||
# kubernetes components secrets on rootCA update procedure
|
||||
KUBE_ROOTCA_SECRET = 'system-kube-rootca-certificate'
|
||||
KUBE_ROOTCA_ISSUER = 'system-kube-rootca-issuer'
|
||||
|
|
|
@ -881,6 +881,10 @@ class CertificateTypeNotFound(NotFound):
|
|||
message = _("No certificate type of %(certtype)s")
|
||||
|
||||
|
||||
class InvalidKubernetesCA(Invalid):
|
||||
message = _("Invalid certificate for kubernetes rootca")
|
||||
|
||||
|
||||
class DockerRegistryCredentialNotFound(NotFound):
|
||||
message = _("Credentials to access local docker registry "
|
||||
"for user %(name)s could not be found.")
|
||||
|
|
|
@ -47,6 +47,7 @@ KUBE_STATE_PARTIAL = 'partial'
|
|||
|
||||
# Kubernetes namespaces
|
||||
NAMESPACE_KUBE_SYSTEM = 'kube-system'
|
||||
NAMESPACE_DEPLOYMENT = 'deployment'
|
||||
|
||||
# Kubernetes control plane components
|
||||
KUBE_APISERVER = 'kube-apiserver'
|
||||
|
@ -80,12 +81,12 @@ KUBE_HOST_UPGRADING_KUBELET_FAILED = 'upgrading-kubelet-failed'
|
|||
KUBE_ROOTCA_UPDATE_STARTED = 'update-started'
|
||||
KUBE_ROOTCA_UPDATE_CERT_UPLOADED = 'update-new-rootca-cert-uploaded'
|
||||
KUBE_ROOTCA_UPDATE_CERT_GENERATED = 'update-new-rootca-cert-generated'
|
||||
KUBE_ROOTCA_UPDATE_UPDATING_PODS_TRUSTBOTHCAS = 'updating-pods-trustBothCAs'
|
||||
KUBE_ROOTCA_UPDATE_UPDATED_PODS_TRUSTBOTHCAS = 'updated-pods-trustBothCAs'
|
||||
KUBE_ROOTCA_UPDATE_UPDATING_PODS_TRUSTBOTHCAS_FAILED = 'updating-pods-trustBothCAs-failed'
|
||||
KUBE_ROOTCA_UPDATE_UPDATING_PODS_TRUSTNEWCA = 'updating-pods-trustNewCA'
|
||||
KUBE_ROOTCA_UPDATE_UPDATED_PODS_TRUSTNEWCA = 'updated-pods-trustNewCA'
|
||||
KUBE_ROOTCA_UPDATE_UPDATING_PODS_TRUSTNEWCA_FAILED = 'updating-pods-trustNewCA-failed'
|
||||
KUBE_ROOTCA_UPDATING_PODS_TRUSTBOTHCAS = 'updating-pods-trustBothCAs'
|
||||
KUBE_ROOTCA_UPDATED_PODS_TRUSTBOTHCAS = 'updated-pods-trustBothCAs'
|
||||
KUBE_ROOTCA_UPDATING_PODS_TRUSTBOTHCAS_FAILED = 'updating-pods-trustBothCAs-failed'
|
||||
KUBE_ROOTCA_UPDATING_PODS_TRUSTNEWCA = 'updating-pods-trustNewCA'
|
||||
KUBE_ROOTCA_UPDATED_PODS_TRUSTNEWCA = 'updated-pods-trustNewCA'
|
||||
KUBE_ROOTCA_UPDATING_PODS_TRUSTNEWCA_FAILED = 'updating-pods-trustNewCA-failed'
|
||||
KUBE_ROOTCA_UPDATE_COMPLETED = 'update-completed'
|
||||
|
||||
# Kubernetes rootca host update states
|
||||
|
@ -554,6 +555,44 @@ class KubeOperator(object):
|
|||
"Namespace %s: %s" % (label, namespace, e))
|
||||
raise
|
||||
|
||||
def get_custom_resource(self, group, version, namespace, plural, name):
|
||||
custom_resource_api = self._get_kubernetesclient_custom_objects()
|
||||
|
||||
try:
|
||||
cert = custom_resource_api.get_namespaced_custom_object(
|
||||
group,
|
||||
version,
|
||||
namespace,
|
||||
plural,
|
||||
name)
|
||||
except ApiException as e:
|
||||
if e.status == httplib.NOT_FOUND:
|
||||
return None
|
||||
else:
|
||||
LOG.error("Fail to access %s:%s. %s" % (namespace, name, e))
|
||||
raise
|
||||
else:
|
||||
return cert
|
||||
|
||||
def apply_custom_resource(self, group, version, namespace, plural, name, body):
|
||||
custom_resource_api = self._get_kubernetesclient_custom_objects()
|
||||
|
||||
# if resource already exists we apply just a patch
|
||||
cert = self.get_custom_resource(group, version, namespace, plural, name)
|
||||
if cert:
|
||||
custom_resource_api.patch_namespaced_custom_object(group,
|
||||
version,
|
||||
namespace,
|
||||
plural,
|
||||
name,
|
||||
body)
|
||||
else:
|
||||
custom_resource_api.create_namespaced_custom_object(group,
|
||||
version,
|
||||
namespace,
|
||||
plural,
|
||||
body)
|
||||
|
||||
def delete_custom_resource(self, group, version, namespace, plural, name):
|
||||
c = self._get_kubernetesclient_custom_objects()
|
||||
body = {}
|
||||
|
|
|
@ -2564,6 +2564,45 @@ def get_aws_ecr_registry_credentials(dbapi, registry, username, password):
|
|||
return dict(username=username, password=password)
|
||||
|
||||
|
||||
def extract_ca_private_key_bytes_from_pem(pem_content):
|
||||
""" Extract key from the PEM file bytes
|
||||
|
||||
:param pem_content: bytes from PEM file where we'll get the key
|
||||
:return base64_crt: extracted key base64 encoded
|
||||
"""
|
||||
begin_search = pem_content.find(constants.BEGIN_PRIVATE_KEY_MARKER)
|
||||
if begin_search < 0:
|
||||
raise exception.InvalidKubernetesCA
|
||||
|
||||
end_search = pem_content.find(constants.END_PRIVATE_KEY_MARKER)
|
||||
if end_search < 0:
|
||||
LOG.info(pem_content)
|
||||
raise exception.InvalidKubernetesCA
|
||||
end_search += len(constants.END_PRIVATE_KEY_MARKER)
|
||||
|
||||
base64_key = base64.b64encode(pem_content[begin_search:end_search])
|
||||
return base64_key
|
||||
|
||||
|
||||
def extract_ca_crt_bytes_from_pem(pem_content):
|
||||
""" Extract certificate from the PEM file bytes
|
||||
|
||||
:param pem_content: bytes from PEM file where we'll get the certificate
|
||||
:return base64_crt: extracted certificate base64 encoded
|
||||
"""
|
||||
begin_search = pem_content.find(constants.BEGIN_CERTIFICATE_MARKER)
|
||||
if begin_search < 0:
|
||||
raise exception.InvalidKubernetesCA
|
||||
|
||||
end_search = pem_content.find(constants.END_CERTIFICATE_MARKER)
|
||||
if end_search < 0:
|
||||
raise exception.InvalidKubernetesCA
|
||||
|
||||
end_search += len(constants.END_CERTIFICATE_MARKER)
|
||||
base64_crt = base64.b64encode(pem_content[begin_search:end_search])
|
||||
return base64_crt
|
||||
|
||||
|
||||
def extract_certs_from_pem(pem_contents):
|
||||
"""
|
||||
Extract certificates from a pem string
|
||||
|
@ -2571,12 +2610,10 @@ def extract_certs_from_pem(pem_contents):
|
|||
:param pem_contents: A string in pem format
|
||||
:return certs: A list of x509 cert objects
|
||||
"""
|
||||
marker = b'-----BEGIN CERTIFICATE-----'
|
||||
|
||||
start = 0
|
||||
certs = []
|
||||
while True:
|
||||
index = pem_contents.find(marker, start)
|
||||
index = pem_contents.find(constants.BEGIN_CERTIFICATE_MARKER, start)
|
||||
if index == -1:
|
||||
break
|
||||
try:
|
||||
|
@ -2589,10 +2626,25 @@ def extract_certs_from_pem(pem_contents):
|
|||
"Failed to load pem x509 certificate"))
|
||||
|
||||
certs.append(cert)
|
||||
start = index + len(marker)
|
||||
start = index + len(constants.BEGIN_CERTIFICATE_MARKER)
|
||||
return certs
|
||||
|
||||
|
||||
def check_cert_validity(cert):
|
||||
"""Perform checks on validity of certificate
|
||||
"""
|
||||
now = datetime.datetime.utcnow()
|
||||
msg = ("certificate is not valid before %s nor after %s" %
|
||||
(cert.not_valid_before, cert.not_valid_after))
|
||||
LOG.info(msg)
|
||||
if now <= cert.not_valid_before or now >= cert.not_valid_after:
|
||||
msg = ("certificate is not valid before %s nor after %s" %
|
||||
(cert.not_valid_before, cert.not_valid_after))
|
||||
LOG.info(msg)
|
||||
return msg
|
||||
return None
|
||||
|
||||
|
||||
def is_ca_cert(cert):
|
||||
"""
|
||||
Check if the certificate is a CA certficate
|
||||
|
@ -2634,6 +2686,55 @@ def get_cert_issuer_hash(cert):
|
|||
return hash_issuer
|
||||
|
||||
|
||||
def get_cert_issuer_string_hash(cert):
|
||||
"""
|
||||
Get the hash value of the cert's issuer DN
|
||||
|
||||
:param cert: the certificate to get issuer from
|
||||
:return: The hash value of the cert's issuer DN
|
||||
"""
|
||||
try:
|
||||
public_bytes = cert.public_bytes(encoding=serialization.Encoding.PEM)
|
||||
cert_c = crypto.load_certificate(crypto.FILETYPE_PEM, public_bytes)
|
||||
|
||||
# get the issuer object from the loaded certificate
|
||||
cert_issuer = cert_c.get_issuer()
|
||||
|
||||
# for each component presented on certificate issuer,
|
||||
# converts the respective name and value for strings and join all
|
||||
# together
|
||||
issuer_attributes = "".join("/{0:s}={1:s}".format(name.decode(),
|
||||
value.decode())
|
||||
for name, value in
|
||||
cert_issuer.get_components())
|
||||
|
||||
# apply the hash function to binary form of the string above and
|
||||
# digest it as a hexdecimal value, and take the first 16 bytes.
|
||||
hashed_attributes = \
|
||||
hashlib.md5(issuer_attributes.encode()).hexdigest()[:16]
|
||||
|
||||
LOG.info("hashed issuer attributes %s from certificate "
|
||||
% hashed_attributes)
|
||||
except Exception:
|
||||
LOG.exception()
|
||||
raise exception.SysinvException(_(
|
||||
"Failed to get certificate issuer hash."))
|
||||
|
||||
return hashed_attributes
|
||||
|
||||
|
||||
def get_cert_serial(cert):
|
||||
try:
|
||||
public_bytes = cert.public_bytes(encoding=serialization.Encoding.PEM)
|
||||
cert_c = crypto.load_certificate(crypto.FILETYPE_PEM, public_bytes)
|
||||
serial_number = cert_c.get_serial_number()
|
||||
except Exception:
|
||||
LOG.exception()
|
||||
raise exception.SysinvException(_(
|
||||
"Failed to get certificate serial number."))
|
||||
return serial_number
|
||||
|
||||
|
||||
def get_cert_subject_hash(cert):
|
||||
"""
|
||||
Get the hash value of the cert's subject DN
|
||||
|
@ -2653,6 +2754,43 @@ def get_cert_subject_hash(cert):
|
|||
return hash_subject
|
||||
|
||||
|
||||
def get_cert_subject_string_hash(cert):
|
||||
"""
|
||||
Get the hash value of the cert's subject DN
|
||||
|
||||
:param cert: the certificate to get subject from
|
||||
:return: The hash value of the cert's subject DN
|
||||
"""
|
||||
try:
|
||||
public_bytes = cert.public_bytes(encoding=serialization.Encoding.PEM)
|
||||
cert_c = crypto.load_certificate(crypto.FILETYPE_PEM, public_bytes)
|
||||
|
||||
# get the subject object from the loaded certificate
|
||||
cert_subject = cert_c.get_subject()
|
||||
|
||||
# for each component presented on certificate subject,
|
||||
# converts the respective name and value for strings and join all
|
||||
# together
|
||||
subject_attributes = "".join("/{0:s}={1:s}".format(name.decode(),
|
||||
value.decode())
|
||||
for name, value in
|
||||
cert_subject.get_components())
|
||||
|
||||
# apply the hash function to binary form of the string above and
|
||||
# digest it as a hexdecimal value, and take the first 16 bytes.
|
||||
hashed_attributes = \
|
||||
hashlib.md5(subject_attributes.encode()).hexdigest()[:16]
|
||||
|
||||
LOG.info("hashed subject attributes %s from certificate "
|
||||
% hashed_attributes)
|
||||
except Exception:
|
||||
LOG.exception()
|
||||
raise exception.SysinvException(_(
|
||||
"Failed to get certificate subject hash."))
|
||||
|
||||
return hashed_attributes
|
||||
|
||||
|
||||
def format_image_filename(device_image):
|
||||
""" Format device image filename """
|
||||
return "{}-{}-{}-{}.bit".format(device_image.bitstream_type,
|
||||
|
|
|
@ -13502,6 +13502,22 @@ class ConductorManager(service.PeriodicService):
|
|||
self.fm_api.clear_fault(fm_constants.FM_ALARM_ID_DEVICE_IMAGE_UPDATE_IN_PROGRESS,
|
||||
entity_instance_id)
|
||||
|
||||
def _get_current_kube_rootca(self):
|
||||
""" Extract current k8s rootca """
|
||||
current_cert = None
|
||||
try:
|
||||
with open('/etc/kubernetes/pki/ca.crt', 'rb') as old_rootca:
|
||||
old_rootca.seek(0, os.SEEK_SET)
|
||||
read_ca = old_rootca.read()
|
||||
old_rootca_cert = cutils.extract_certs_from_pem(read_ca)[0]
|
||||
hash_subject = cutils.get_cert_issuer_string_hash(old_rootca_cert)
|
||||
serial_number = cutils.get_cert_serial(old_rootca_cert)
|
||||
current_cert = "%s-%s" % (str(hash_subject), str(serial_number))
|
||||
except Exception:
|
||||
msg = "Extracting information regarding current k8s rootca failed"
|
||||
return dict(success="", error=msg)
|
||||
return current_cert
|
||||
|
||||
def fpga_device_update_by_host(self, context,
|
||||
host_uuid, fpga_device_dict_array):
|
||||
"""Create FPGA devices for an ihost with the supplied data.
|
||||
|
@ -13708,6 +13724,177 @@ class ConductorManager(service.PeriodicService):
|
|||
LOG.info(output)
|
||||
return output
|
||||
|
||||
def _create_kube_rootca_resources(self, certificate, key):
|
||||
""" A method to create new resources to store new kubernetes
|
||||
rootca data.
|
||||
|
||||
:param certificate: the certificate to be stored in TLS secret
|
||||
:param key: the certificate key to be stored in TLS secret
|
||||
:return: An error message if method is not successful, otherwhise None
|
||||
"""
|
||||
kube_operator = kubernetes.KubeOperator()
|
||||
|
||||
body = {
|
||||
'apiVersion': 'v1',
|
||||
'type': 'kubernetes.io/tls',
|
||||
'kind': 'Secret',
|
||||
'metadata': {
|
||||
'name': constants.KUBE_ROOTCA_SECRET,
|
||||
'namespace': kubernetes.NAMESPACE_DEPLOYMENT
|
||||
},
|
||||
'data': {
|
||||
'tls.crt': certificate,
|
||||
'tls.key': key
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
secret = kube_operator.kube_get_secret(constants.KUBE_ROOTCA_SECRET,
|
||||
kubernetes.NAMESPACE_DEPLOYMENT)
|
||||
if secret is not None:
|
||||
kube_operator.kube_delete_secret(constants.KUBE_ROOTCA_SECRET,
|
||||
kubernetes.NAMESPACE_DEPLOYMENT)
|
||||
kube_operator.kube_create_secret(kubernetes.NAMESPACE_DEPLOYMENT, body)
|
||||
except Exception as e:
|
||||
msg = "Creation of kube-rootca secret failed: %s" % str(e)
|
||||
LOG.error(msg)
|
||||
return msg
|
||||
|
||||
body = {
|
||||
'apiVersion': 'cert-manager.io/v1alpha2',
|
||||
'kind': 'Issuer',
|
||||
'metadata': {
|
||||
'name': constants.KUBE_ROOTCA_ISSUER,
|
||||
'namespace': kubernetes.NAMESPACE_DEPLOYMENT
|
||||
},
|
||||
'spec': {
|
||||
'ca': {
|
||||
'secretName': constants.KUBE_ROOTCA_SECRET
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
kube_operator.apply_custom_resource('cert-manager.io',
|
||||
'v1alpha2',
|
||||
kubernetes.NAMESPACE_DEPLOYMENT,
|
||||
'issuers',
|
||||
constants.KUBE_ROOTCA_ISSUER,
|
||||
body)
|
||||
except Exception as e:
|
||||
msg = "Not successfull applying issuer: %s" % str(e)
|
||||
return msg
|
||||
|
||||
def _precheck_save_kubernetes_rootca_cert(self, update, temp_pem_contents):
|
||||
""" This method intends to do a series of validations to allow the upload
|
||||
of a new rootca for kubernetes. These validations are respective to the
|
||||
procedure itself or the new ca file that is being uploaded.
|
||||
|
||||
:param update: actual entry of kube rootca update procedure from DB
|
||||
:param temp_pem_contents: content of the file uploaded to update kube rootca
|
||||
:return: A dictionaire with a new_cert if successful and eventual error message
|
||||
"""
|
||||
|
||||
if update.state != kubernetes.KUBE_ROOTCA_UPDATE_STARTED:
|
||||
msg = "A new root CA certificate already exists"
|
||||
return dict(success="", error=msg)
|
||||
|
||||
if update.to_rootca_cert:
|
||||
LOG.info("root CA target with serial number %s will be overwritten"
|
||||
% update.to_rootca_cert)
|
||||
|
||||
# extract the certificate contained in PEM file
|
||||
try:
|
||||
cert = cutils.extract_certs_from_pem(temp_pem_contents)[0]
|
||||
except Exception as e:
|
||||
msg = "Failed to extract certificate from file: %s" % str(e)
|
||||
return dict(success="", error=msg)
|
||||
|
||||
if not cert:
|
||||
msg = "No certificate have been added, " \
|
||||
"no valid certificate found in file."
|
||||
LOG.info(msg)
|
||||
return dict(success="", error=msg)
|
||||
|
||||
# validate certificate
|
||||
msg = cutils.check_cert_validity(cert)
|
||||
|
||||
if msg is not None:
|
||||
return dict(success="", error=msg)
|
||||
|
||||
is_ca = cutils.is_ca_cert(cert)
|
||||
if not is_ca:
|
||||
msg = "The certificate in the file is not a CA certificate"
|
||||
return dict(success="", error=msg)
|
||||
|
||||
# extract information regarding the new rootca
|
||||
try:
|
||||
hash_subject = cutils.get_cert_issuer_string_hash(cert)
|
||||
serial_number = cutils.get_cert_serial(cert)
|
||||
new_cert = '%s-%s' % (hash_subject, serial_number)
|
||||
except Exception:
|
||||
msg = "Failed to extract respective subject and serial number"
|
||||
return dict(success="", error=msg)
|
||||
|
||||
return dict(success=new_cert, error="")
|
||||
|
||||
def save_kubernetes_rootca_cert(self, context, ca_file):
|
||||
"""
|
||||
Save a new uploaded kubernetes rootca for update procedure
|
||||
:param context: request context
|
||||
:param ca_file: a stream representing the PEM file uploaded
|
||||
"""
|
||||
|
||||
# ca_file has to be in bytes format for extract information
|
||||
if not isinstance(ca_file, bytes):
|
||||
temp_pem_contents = ca_file.encode("utf-8")
|
||||
else:
|
||||
temp_pem_contents = ca_file
|
||||
|
||||
try:
|
||||
update = self.dbapi.kube_rootca_update_get_one()
|
||||
except exception.NotFound:
|
||||
msg = "Kubernetes root CA update not started"
|
||||
LOG.error(msg)
|
||||
return dict(success="", error=msg)
|
||||
|
||||
result = self._precheck_save_kubernetes_rootca_cert(update, temp_pem_contents)
|
||||
if result.get("error"):
|
||||
msg = result.get("error")
|
||||
return dict(success="", error=msg)
|
||||
else:
|
||||
new_cert = result.get("success")
|
||||
|
||||
# extract current k8s rootca
|
||||
current_cert = self._get_current_kube_rootca()
|
||||
if not current_cert:
|
||||
msg = "Not able to get the current kube rootca"
|
||||
return dict(success="", error=msg)
|
||||
|
||||
try:
|
||||
certificate = cutils.extract_ca_crt_bytes_from_pem(temp_pem_contents)
|
||||
except exception.InvalidKubernetesCA:
|
||||
msg = "Invalid certificate format"
|
||||
return dict(success="", error=msg)
|
||||
|
||||
try:
|
||||
key = cutils.extract_ca_private_key_bytes_from_pem(temp_pem_contents)
|
||||
except exception.InvalidKubernetesCA:
|
||||
msg = "Failed to extract key from certificate file"
|
||||
return dict(success="", error=msg)
|
||||
|
||||
msg = self._create_kube_rootca_resources(certificate, key)
|
||||
if msg is not None:
|
||||
return dict(success="", error=msg)
|
||||
|
||||
# update db
|
||||
update_obj = {'state': kubernetes.KUBE_ROOTCA_UPDATE_CERT_UPLOADED,
|
||||
'from_rootca_cert': current_cert,
|
||||
'to_rootca_cert': new_cert}
|
||||
|
||||
r = self.dbapi.kube_rootca_update_update(update.id, update_obj)
|
||||
return dict(success=r.to_rootca_cert, error="")
|
||||
|
||||
def mtc_action_apps_semantic_checks(self, context, action):
|
||||
"""Call semantic check for maintenance actions of each app.
|
||||
Fail if at least one app rejects the action.
|
||||
|
|
|
@ -2220,3 +2220,12 @@ class ConductorAPI(sysinv.openstack.common.rpc.proxy.RpcProxy):
|
|||
"""
|
||||
return self.call(context,
|
||||
self.make_msg('update_dnsmasq_config'))
|
||||
|
||||
def save_kubernetes_rootca_cert(self, context, certificate_file):
|
||||
"""Save the new uploaded k8s root CA certificate
|
||||
|
||||
:param context: request context.
|
||||
:certificate_file: the new rootca PEM file
|
||||
"""
|
||||
return self.call(context, self.make_msg('save_kubernetes_rootca_cert',
|
||||
ca_file=certificate_file))
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAthLAcpLktKA5jm44TGVxza/M1Yt4/67wKXDHqCOixZQ4w7Pz
|
||||
sS62YjpBInrs4taRX0GbDKondaur88dPVSZ3/7vXQ7RJCELnTSpM5KDN5NExzkte
|
||||
Kr0zNrt8d0/h3UoWS9Jgl3/i4YIVu7l+spUNNV7YF0Nr9MUifRKTj3Cg5B5R4NTX
|
||||
oXSgaYY5kL2MRF0YQ/FhYqz3nTCm2yogNM4R8FGIJYVbPAnA2wol06GL/IwMYght
|
||||
wEdL7ocjcXVmsv7XidtI+Dp7BQWRs35w7atx+fr1Tr4jO6npkvzfUMgSy4d3c9R4
|
||||
jhUaeMp62gEPp1MbZydlBWxM2s4q4kkgGHRMrQIDAQABAoIBAF77p1PeF9u23m/U
|
||||
RiBsp5LjDFu2t/fCvl0QDchEVuz15ysJHK8pLFJQC5y+PggUYaAs7IMN3SoA1eKF
|
||||
7ngAaoeJ6cHTMmpR5LKXx6dZ0C93hqEVJlnre+Uop8TicnTr6nfBl0xRlf2IzGez
|
||||
XEozgcF+6gIw1QfLM7PF1h71Zam62RQuRAqjTpGUz0a5XuAs7H5MwLYGM8jebCNy
|
||||
fGgi8NSHzGi8P83rNb3M486Tavlfh/OM7aM5S+Rf8PuG7BXvAzx9L4zf4Zo4L8Ud
|
||||
Sw+l87SK62Lw0eMRq5obSDre4cXSJWGOqG8lAs/LfYc51n+mPcirxFvoiFGYspi8
|
||||
lgVPGc0CgYEA7xDg/7yDLQ2QDGDP5ATi8Ea/FNORNo6ie+BIneZxrq822JRRJzoj
|
||||
GstuZBoMeMKuSIhIOgXOAUAUNpKNV/pgxvdldZPkdR3JhZLHBO6AJ6Llj8M1shAt
|
||||
wCKh2zKdPc9ACk+W1Ou6lgok6b0pRmhvJv+Rr9GlR9wXSke9FL/SiQcCgYEAwvhk
|
||||
N+kze9+pWGrvTM9728ETP0Mq/Fg55I0D8F82msXSEdcmYGx5fJvGbNMshNy+jbLD
|
||||
RUlAXmJfzmgqdGJKPNf5Rgl52Tr42h9BgqXTIGPO4nrXVa7apS8KWP2jt640VqSa
|
||||
iGM6auC6elJsO8w1nQAVnxnJwiKzV3gWPaKO06sCgYBAugExPIkHmbR2pX+j7O7E
|
||||
v2Lc8KtQai3z/DWtCsec1DO1T/Lo/ASlLI8m6yaVS6CEYuGrVAcCr6bJX8SFHXU2
|
||||
aaU+wFwKmZYGZEcePrTUBnbBBclz/I1mh/nqrzmDkql0IThlTa2nEfgMkPqr5Xqy
|
||||
xF9dixWE70IfCm1XQNhv4QKBgAOnO9mAWSKdEkNB3bIGwT9g4sdwrsGDtbH+onBC
|
||||
mHdV9ZW3/lQYND6NfK5VVqQ2rqthCh+mO7qJBVqMwR7lKJbzRQx26P2VCUytAUE9
|
||||
cjNNK3c67gYA/L/TndIFDqhGb1ygQPUFRvbxtwzLtpN4RBjpA36zsQAePlYJPgFx
|
||||
plN5AoGBAJQUWaSb/dyPnekFfuTyXrkFRSL/rh/VXCABtIVqGVnAC7rD3vrW7ZFd
|
||||
1yZwJPu8P7xrQ0e6QdzF1C/2TLnvVsPwgJYM0GR1rhdMuQ9GCdxg8GF0tMFbzngY
|
||||
OdUmTvDa6j1rGc3yVwihZ8Z3RCM+VdYVA7V2uiB9YhmcATcnh+AN
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,81 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIE7TCCAtWgAwIBAgIDAMYmMA0GCSqGSIb3DQEBCwUAMB8xEDAOBgNVBAoMB0V0
|
||||
Y2QgQ0ExCzAJBgNVBAMMAmNhMB4XDTIxMDMyOTE3NDYzM1oXDTMxMDMyNzE3NDYz
|
||||
M1owHzEQMA4GA1UECgwHRXRjZCBDQTELMAkGA1UEAwwCY2EwggIiMA0GCSqGSIb3
|
||||
DQEBAQUAA4ICDwAwggIKAoICAQC53t23VB7sngnQOO78GBmG1uOAEMWcM/MA9Tjh
|
||||
bDV4R5BfMK7OPYQ5lEkLcA10Rk7MxCTPZ2R+9YDB/gXnb0sZRN2B46bYN7YQ6tAc
|
||||
ZQosxt49FgqcoyRUWW5gxisHHFdirez8GZLHSfHdxj2cLc41HD0KyHVQJp+2s6bR
|
||||
ipAEtrWHYp9ppgkmY0BXvozWEVBiJnb/nC2nsWyq6kuDbyc2h8ZADrETm5jdt+CC
|
||||
HnBiv2r3teEkZiqbShy/Dh1jPzcpvmXYEPDhBDkKjCtPXOG4S8c2qQ6lqMdZhTqZ
|
||||
K9GP5H+0N0bQd9RmDG81OuMMgXgPs2kjp2z7gVvtr2qLAsDeCiL9tyRuQJO1Q7Y8
|
||||
pHAvlKDnhTYy0JiN62Wi93xpgCKirqH85BKhSF+8Dv2QFOnqGCRt27d17yInalj4
|
||||
j0/YVhA2nH3wsTwcWMswcYRIrWUy9Ni3Yj/PwZp6ANUR3FaZuBjFMUFhURNvoC3q
|
||||
+/2zv2FrRKCDos4Lddd0p7g+BjAwaBq9ldU8vFxcJMe8ECIbp8FER6g9T5wq6MWv
|
||||
FdQsvKzH6WlkCcH0u27uvwBNDGkfUUNfvJEegkFfiqYk/GpvDyYMO89ZmpLTO4Oh
|
||||
55641tHUh97eNpGfcMFXVhgRtcA01lIfghj7sKEEoTlVjprta+yuiAdp/8QHriRy
|
||||
BxYNNQIDAQABozIwMDANBgNVHREEBjAEggJjYTALBgNVHQ8EBAMCAoQwEgYDVR0T
|
||||
AQH/BAgwBgEB/wIBATANBgkqhkiG9w0BAQsFAAOCAgEAj8QylagndcbqRR0ZHXtx
|
||||
/HZ5/bli3pkaUF0FCEHoypEOf6aIYHgjyFuoBaAPLnncsW0B4828AHx+nfWfNFnz
|
||||
o1FF5/t5LUi9dCLPRXV0CL2Xnhb7QmNowFsbYb7W/xAkcMM4vOKDE6Z4HjUX8Ypj
|
||||
5fHBIGYxEhma+BJC+02JRycGTa3Nr+1yDXfMBnUKgXVk9erz+krXvIxK7Kxc1cEi
|
||||
NmpkJwKZtxx1PcMcsY071ahMO0bxcVS8JTn1jVG//yUYipYcaZqO8MvG8uYOFtCI
|
||||
OHIKD+8e8H7mr83K+rcctI9+vkbkTSCs3CLeozyt1qZYRoh599rtjTMhyNDrsB75
|
||||
bcYzXJIuP18xZAlQW7wYf1FHvWMGsZhBZBMwauOix86TfuoYpPfNyCRHysUQSkjy
|
||||
BhWs2KPcNqWG/bS0l+ZAYMcZC1e9GnTMcXohDDij/QjUNncaSiikMoO3xrCajhkp
|
||||
thGjWNvwQ2i1cWYSAWtm3W5w0TPr2bVf5nCZ29QgPZvZn0ziIrgYgOKRwCEcQQG8
|
||||
y95klQRXr9ErU3fk1C01BlsLdzcRVC2EJh7FyTsNraUXFLiUo3xtE8XwR/rNWPK9
|
||||
gAVNPIx62soLQfTUZKRIW0C2esqT7zNxQcbIswRqqe7fr2EG4Ab1p9PzXQv3L1HM
|
||||
HNnJdMzekIAiKxt8obIAki4=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
ACeCTlUNHHFB14hL2b/ZuZ4dpc3ayw==
|
||||
-----END PRIVATE KEY-----
|
|
@ -2,9 +2,10 @@
|
|||
Tests for the API /kube_rootca_update/ methods.
|
||||
"""
|
||||
|
||||
import json
|
||||
import mock
|
||||
import os
|
||||
from six.moves import http_client
|
||||
|
||||
from sysinv.common import constants
|
||||
from sysinv.common import health
|
||||
from sysinv.common import kubernetes
|
||||
|
@ -34,6 +35,9 @@ class FakeConductorAPI(object):
|
|||
|
||||
def __init__(self):
|
||||
self.service = ConductorManager('test-host', 'test-topic')
|
||||
self.save_kubernetes_rootca_cert = self.fake_config_certificate
|
||||
self.config_certificate_return = None
|
||||
self.platcert_k8s_secret_value = False
|
||||
|
||||
def get_system_health(self, context, force=False, upgrade=False,
|
||||
kube_upgrade=False, kube_rootca_update=False,
|
||||
|
@ -46,6 +50,12 @@ class FakeConductorAPI(object):
|
|||
kube_rootca_update=kube_rootca_update,
|
||||
alarm_ignore_list=alarm_ignore_list)
|
||||
|
||||
def fake_config_certificate(self, context, pem):
|
||||
return self.config_certificate_return
|
||||
|
||||
def setup_config_certificate(self, data):
|
||||
self.config_certificate_return = data
|
||||
|
||||
|
||||
class TestKubeRootCAUpdate(base.FunctionalTest):
|
||||
|
||||
|
@ -119,6 +129,8 @@ class TestPostKubeRootUpdate(TestKubeRootCAUpdate,
|
|||
# Verify that the kubernetes rootca update has the expected attributes
|
||||
self.assertEqual(result.json['state'],
|
||||
kubernetes.KUBE_ROOTCA_UPDATE_STARTED)
|
||||
self.assertNotEqual(result.json['from_rootca_cert'], None)
|
||||
self.assertEqual(result.json['from_rootca_cert'], 'oldCertSerial')
|
||||
|
||||
def test_create_rootca_update_unhealthy_from_alarms(self):
|
||||
""" Test creation of kube rootca update while there are alarms"""
|
||||
|
@ -192,3 +204,41 @@ class TestPostKubeRootUpdate(TestKubeRootCAUpdate,
|
|||
self.assertEqual(http_client.BAD_REQUEST, result.status_int)
|
||||
self.assertIn("rootca update cannot be done while a platform upgrade",
|
||||
result.json['error_message'])
|
||||
|
||||
|
||||
class TestKubeRootCAUpload(TestKubeRootCAUpdate,
|
||||
dbbase.ProvisionedControllerHostTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestKubeRootCAUpload, self).setUp()
|
||||
self.fake_conductor_api.service.dbapi = self.dbapi
|
||||
|
||||
@mock.patch.object(kubernetes.KubeOperator,
|
||||
'kube_create_secret')
|
||||
@mock.patch.object(kubernetes.KubeOperator,
|
||||
'apply_custom_resource')
|
||||
def test_upload_rootca(self, mock_create_secret, mock_create_custom_resource):
|
||||
dbutils.create_test_kube_rootca_update(state=kubernetes.KUBE_ROOTCA_UPDATE_STARTED)
|
||||
certfile = os.path.join(os.path.dirname(__file__), "data",
|
||||
'rootca-with-key.pem')
|
||||
|
||||
fake_save_rootca_return = {'success': '137813-123', 'error': ''}
|
||||
|
||||
self.fake_conductor_api.\
|
||||
setup_config_certificate(fake_save_rootca_return)
|
||||
|
||||
files = [('file', certfile)]
|
||||
response = self.post_with_files('%s/%s' % ('/kube_rootca_update', 'upload'),
|
||||
{},
|
||||
upload_files=files,
|
||||
headers={'User-Agent': 'sysinv-test'},
|
||||
expect_errors=False)
|
||||
|
||||
self.assertEqual(response.content_type, 'application/json')
|
||||
self.assertEqual(response.status_code, http_client.OK)
|
||||
|
||||
resp = json.loads(response.body)
|
||||
|
||||
self.assertTrue(resp.get('success'))
|
||||
self.assertEqual(resp.get('success'), fake_save_rootca_return.get('success'))
|
||||
self.assertFalse(resp.get('error'))
|
||||
|
|
|
@ -27,6 +27,9 @@ import os.path
|
|||
import tsconfig.tsconfig as tsc
|
||||
import uuid
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
from sysinv.agent import rpcapi as agent_rpcapi
|
||||
from sysinv.common import constants
|
||||
from sysinv.common import device as dconstants
|
||||
|
@ -68,6 +71,25 @@ class FakePopen(object):
|
|||
|
||||
class ManagerTestCase(base.DbTestCase):
|
||||
|
||||
@staticmethod
|
||||
def extract_certs_from_pem_file(certfile):
|
||||
""" extract certificates from a X509 PEM file
|
||||
"""
|
||||
marker = b'-----BEGIN CERTIFICATE-----'
|
||||
with open(certfile, 'rb') as f:
|
||||
pem_contents = f.read()
|
||||
start = 0
|
||||
certs = []
|
||||
while True:
|
||||
index = pem_contents.find(marker, start)
|
||||
if index == -1:
|
||||
break
|
||||
cert = x509.load_pem_x509_certificate(pem_contents[index::],
|
||||
default_backend())
|
||||
certs.append(cert)
|
||||
start = index + len(marker)
|
||||
return certs
|
||||
|
||||
def setUp(self):
|
||||
super(ManagerTestCase, self).setUp()
|
||||
|
||||
|
@ -126,6 +148,13 @@ class ManagerTestCase(base.DbTestCase):
|
|||
self._ready_to_apply_runtime_config
|
||||
self.addCleanup(self.ready_to_apply_runtime_config_patcher.stop)
|
||||
|
||||
# Mock check_cert_validity
|
||||
def mock_cert_validity(obj):
|
||||
return None
|
||||
self.mocked_cert_validity = mock.patch.object(cutils, 'check_cert_validity', mock_cert_validity)
|
||||
self.mocked_cert_validity.start()
|
||||
self.addCleanup(self.mocked_cert_validity.stop)
|
||||
|
||||
# Mock agent config_apply_runtime_manifest
|
||||
def mock_agent_config_apply_runtime_manifest(obj, context, config_uuid,
|
||||
config_dict):
|
||||
|
@ -1840,6 +1869,124 @@ class ManagerTestCase(base.DbTestCase):
|
|||
for key in fpga_dev_dict_update:
|
||||
self.assertEqual(dev[key], fpga_dev_dict_update[key])
|
||||
|
||||
def test_upload_rootca(self):
|
||||
mock_kube_create_secret = mock.MagicMock()
|
||||
p = mock.patch(
|
||||
'sysinv.common.kubernetes.KubeOperator.kube_create_secret',
|
||||
mock_kube_create_secret)
|
||||
p.start()
|
||||
self.addCleanup(p.stop)
|
||||
|
||||
mock_kube_create_issuer = mock.MagicMock()
|
||||
q = mock.patch(
|
||||
'sysinv.common.kubernetes.KubeOperator.apply_custom_resource',
|
||||
mock_kube_create_issuer)
|
||||
q.start()
|
||||
self.addCleanup(q.stop)
|
||||
|
||||
mock_get_current_kube_rootca = mock.MagicMock()
|
||||
z = mock.patch(
|
||||
'sysinv.conductor.manager.ConductorManager._get_current_kube_rootca',
|
||||
mock_get_current_kube_rootca
|
||||
)
|
||||
self.mock_current_kube_rootca = z.start()
|
||||
self.mock_current_kube_rootca.return_value = 'test'
|
||||
self.addCleanup(z.stop)
|
||||
|
||||
mock_get_secret = mock.MagicMock()
|
||||
l = mock.patch(
|
||||
'sysinv.common.kubernetes.KubeOperator.kube_get_secret',
|
||||
mock_get_secret
|
||||
)
|
||||
self.mock_kube_get_secret = l.start()
|
||||
self.addCleanup(l.stop)
|
||||
|
||||
mock_delete_secret = mock.MagicMock()
|
||||
w = mock.patch(
|
||||
'sysinv.common.kubernetes.KubeOperator.kube_delete_secret',
|
||||
mock_delete_secret
|
||||
)
|
||||
self.mock_secret_delete = w.start()
|
||||
self.addCleanup(w.stop)
|
||||
|
||||
utils.create_test_kube_rootca_update(state=kubernetes.KUBE_ROOTCA_UPDATE_STARTED)
|
||||
file = os.path.join(os.path.dirname(__file__), "../api", "data",
|
||||
'rootca-with-key.pem')
|
||||
with open(file, 'rb') as certfile:
|
||||
certfile.seek(0, os.SEEK_SET)
|
||||
f = certfile.read()
|
||||
resp = self.service.save_kubernetes_rootca_cert(self.context, f)
|
||||
|
||||
self.assertTrue(resp.get('success'))
|
||||
self.assertFalse(resp.get('error'))
|
||||
|
||||
def test_upload_rootca_only_key(self):
|
||||
mock_get_current_kube_rootca = mock.MagicMock()
|
||||
z = mock.patch(
|
||||
'sysinv.conductor.manager.ConductorManager._get_current_kube_rootca',
|
||||
mock_get_current_kube_rootca
|
||||
)
|
||||
self.mock_current_kube_rootca = z.start()
|
||||
self.mock_current_kube_rootca.return_value = 'test'
|
||||
self.addCleanup(z.stop)
|
||||
|
||||
utils.create_test_kube_rootca_update(state=kubernetes.KUBE_ROOTCA_UPDATE_STARTED)
|
||||
file = os.path.join(os.path.dirname(__file__), "../api", "data",
|
||||
'only_key.pem')
|
||||
|
||||
with open(file, 'rb') as certfile:
|
||||
certfile.seek(0, os.SEEK_SET)
|
||||
f = certfile.read()
|
||||
resp = self.service.save_kubernetes_rootca_cert(self.context, f)
|
||||
|
||||
self.assertTrue(resp.get('error'))
|
||||
self.assertIn("Failed to extract certificate from file", resp.get('error'))
|
||||
|
||||
def test_upload_rootca_not_ca_certificate(self):
|
||||
mock_get_current_kube_rootca = mock.MagicMock()
|
||||
z = mock.patch(
|
||||
'sysinv.conductor.manager.ConductorManager._get_current_kube_rootca',
|
||||
mock_get_current_kube_rootca
|
||||
)
|
||||
self.mock_current_kube_rootca = z.start()
|
||||
self.mock_current_kube_rootca.return_value = 'test'
|
||||
self.addCleanup(z.stop)
|
||||
|
||||
utils.create_test_kube_rootca_update(state=kubernetes.KUBE_ROOTCA_UPDATE_STARTED)
|
||||
|
||||
file = os.path.join(os.path.dirname(__file__), "../api", "data", 'cert-with-key-SAN.pem')
|
||||
with open(file, 'rb') as certfile:
|
||||
certfile.seek(0, os.SEEK_SET)
|
||||
f = certfile.read()
|
||||
resp = self.service.save_kubernetes_rootca_cert(self.context, f)
|
||||
self.assertTrue(resp.get('error'))
|
||||
self.assertIn("certificate in the file is not a CA certificate", resp.get('error'))
|
||||
|
||||
def test_upload_rootca_not_in_progress(self):
|
||||
file = os.path.join(os.path.dirname(__file__), "../api", "data",
|
||||
'rootca-with-key.pem')
|
||||
|
||||
with open(file, 'rb') as certfile:
|
||||
certfile.seek(0, os.SEEK_SET)
|
||||
f = certfile.read()
|
||||
resp = self.service.save_kubernetes_rootca_cert(self.context, f)
|
||||
|
||||
self.assertTrue(resp.get('error'))
|
||||
self.assertIn("Kubernetes root CA update not started", resp.get('error'))
|
||||
|
||||
def test_upload_rootca_advanced_state(self):
|
||||
utils.create_test_kube_rootca_update(state=kubernetes.KUBE_ROOTCA_UPDATING_PODS_TRUSTBOTHCAS)
|
||||
file = os.path.join(os.path.dirname(__file__), "../api", "data",
|
||||
'rootca-with-key.pem')
|
||||
|
||||
with open(file, 'rb') as certfile:
|
||||
certfile.seek(0, os.SEEK_SET)
|
||||
f = certfile.read()
|
||||
resp = self.service.save_kubernetes_rootca_cert(self.context, f)
|
||||
|
||||
self.assertTrue(resp.get('error'))
|
||||
self.assertIn("new root CA certificate already exists", resp.get('error'))
|
||||
|
||||
def test_device_update_image_status(self):
|
||||
|
||||
mock_host_device_image_update_next = mock.MagicMock()
|
||||
|
|
Loading…
Reference in New Issue