951 lines
42 KiB
Python
951 lines
42 KiB
Python
#
|
|
# Copyright (c) 2021 Wind River Systems, Inc.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
|
|
|
|
import datetime
|
|
import os
|
|
import pecan
|
|
import re
|
|
import six
|
|
import wsme
|
|
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
|
|
from sysinv.api.controllers.v1 import collection
|
|
from sysinv.api.controllers.v1 import link
|
|
from sysinv.api.controllers.v1 import types
|
|
from sysinv.api.controllers.v1 import utils
|
|
from sysinv.common import constants
|
|
from sysinv.common import dc_api
|
|
from sysinv.common import exception
|
|
from sysinv.common import kubernetes
|
|
from sysinv.common import utils as cutils
|
|
from sysinv._i18n import _
|
|
from wsme import types as wtypes
|
|
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
LOCK_KUBE_ROOTCA_CONTROLLER = 'KubeRootCAController'
|
|
|
|
|
|
class KubeRootCAUpdatePatchType(types.JsonPatchType):
|
|
|
|
@staticmethod
|
|
def mandatory_attrs():
|
|
return ['/state']
|
|
|
|
|
|
class KubeRootCAGenerateController(rest.RestController):
|
|
""" API representation of a Kubernetes Generate Root CA Certificate"""
|
|
|
|
def get_subject(self, subject):
|
|
""" Takes a subject formatted string and convert it to a
|
|
dictionary corresponding to the respective parameter to be
|
|
added in the certificate and its value added.
|
|
|
|
:params subject: a formatted string corresponding to subject
|
|
data
|
|
:returns: a dictionary with the key-value pair of each
|
|
parameter. In the case where a not supported parameter is
|
|
added to the string the method will throw an exception
|
|
indicating the error.
|
|
"""
|
|
params_supported = ['C', 'OU', 'O', 'ST', 'CN', 'L']
|
|
if subject is None:
|
|
return {}
|
|
subject_pairs = re.findall(r"([^=]+=[^=]+)(?:\s|$)", subject)
|
|
subject_dict = {}
|
|
for pair_value in subject_pairs:
|
|
key, value = pair_value.split("=")
|
|
subject_dict[key] = value
|
|
|
|
if not all([param in params_supported for param in subject_dict.keys()]):
|
|
raise wsme.exc.ClientSideError(_("There are parameters not supported "
|
|
"for the certificate subject specification. "
|
|
"The subject parameter has to be in the "
|
|
"format of 'C=<Country> ST=<State/Province> "
|
|
"L=<Locality> O=<Organization> OU=<OrganizationUnit> "
|
|
"CN=<commonName>"))
|
|
if 'CN' not in list(subject_dict.keys()):
|
|
raise wsme.exc.ClientSideError(_("The CN=<commonName> parameter is required to be "
|
|
"specified in subject argument"))
|
|
return subject_dict
|
|
|
|
def _calculate_duration(self, expiry_date):
|
|
if expiry_date is None:
|
|
return None
|
|
try:
|
|
date = datetime.datetime.strptime(expiry_date, "%Y-%m-%d")
|
|
except ValueError:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"expiry_date %s doesn't match format YYYY-MM-DD" % expiry_date))
|
|
delta = date - datetime.datetime.now()
|
|
|
|
# we sum one day (24 hours) to accomplish the certificate expiry
|
|
# during the day specified by the user
|
|
duration = (delta.days * 24 + 24)
|
|
|
|
# Cert-manager manages certificates and renew them some time
|
|
# before it expires. Along this procedure we set renewBefore
|
|
# parameter for 24h, so we are checking if the duration sent
|
|
# has at least this amount of time. This is needed to avoid
|
|
# cert-manager to block the creation of the resources.
|
|
if duration <= 24:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"New k8s rootCA should have at least 24 hours of "
|
|
"validation before expiry."))
|
|
return duration
|
|
|
|
@cutils.synchronized(LOCK_KUBE_ROOTCA_CONTROLLER)
|
|
@wsme_pecan.wsexpose(wtypes.text, wtypes.text, wtypes.text)
|
|
def post(self, expiry_date, subject):
|
|
duration = self._calculate_duration(expiry_date)
|
|
subject_params = self.get_subject(subject)
|
|
output = pecan.request.rpcapi.generate_kubernetes_rootca_cert(
|
|
pecan.request.context, subject_params, duration)
|
|
return output
|
|
|
|
|
|
class KubeRootCAUploadController(rest.RestController):
|
|
""" API representation of a Kubernetes Upload Root CA Certificate"""
|
|
|
|
@cutils.synchronized(LOCK_KUBE_ROOTCA_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):
|
|
"""API representation of a Kubernetes RootCA Update."""
|
|
|
|
id = int
|
|
"Unique ID for this entry"
|
|
|
|
uuid = types.uuid
|
|
"Unique UUID for this entry"
|
|
|
|
from_rootca_cert = wtypes.text
|
|
"The from certificate for the kubernetes rootCA update"
|
|
|
|
to_rootca_cert = wtypes.text
|
|
"The to certificate for the kubernetes rootCA update"
|
|
|
|
state = wtypes.text
|
|
"Kubernetes rootCA update state"
|
|
|
|
capabilities = {wtypes.text: utils.ValidTypes(wtypes.text,
|
|
six.integer_types)}
|
|
"Additional properties to be used in kube_rootca_update operations"
|
|
|
|
links = [link.Link]
|
|
"A list containing a self link and associated kubernetes rootca update links"
|
|
|
|
def __init__(self, **kwargs):
|
|
self.fields = list(objects.kube_rootca_update.fields.keys())
|
|
for k in self.fields:
|
|
if not hasattr(self, k):
|
|
continue
|
|
setattr(self, k, kwargs.get(k, wtypes.Unset))
|
|
|
|
@classmethod
|
|
def convert_with_links(cls, rpc_kube_rootca_update, expand=True):
|
|
kube_rootca_update = KubeRootCAUpdate(**rpc_kube_rootca_update.as_dict())
|
|
if not expand:
|
|
kube_rootca_update.unset_fields_except(['uuid', 'from_rootca_cert',
|
|
'to_rootca_cert', 'state'])
|
|
|
|
kube_rootca_update.links = [
|
|
link.Link.make_link('self', pecan.request.host_url,
|
|
'kube_rootca_update', kube_rootca_update.uuid),
|
|
link.Link.make_link('bookmark',
|
|
pecan.request.host_url,
|
|
'kube_rootca_update', kube_rootca_update.uuid,
|
|
bookmark=True)
|
|
]
|
|
return kube_rootca_update
|
|
|
|
|
|
class KubeRootCAHostUpdate(base.APIBase):
|
|
"""API representation of a Kubernetes RootCA Host Update."""
|
|
|
|
id = int
|
|
"Unique ID for this entry"
|
|
|
|
uuid = types.uuid
|
|
"Unique UUID for this entry"
|
|
|
|
target_rootca_cert = wtypes.text
|
|
"The target certificate for the kubernetes rootCA host update"
|
|
|
|
effective_rootca_cert = wtypes.text
|
|
"The current certificate of the kubernetes rootCA on this host"
|
|
|
|
state = wtypes.text
|
|
"Kubernetes rootCA host update state"
|
|
|
|
hostname = wtypes.text
|
|
"Represent the hostname of the host"
|
|
|
|
personality = wtypes.text
|
|
"Represent the personality of the host"
|
|
|
|
capabilities = {wtypes.text: utils.ValidTypes(wtypes.text,
|
|
six.integer_types)}
|
|
"Additional properties to be used in kube_rootca_host_update operations"
|
|
|
|
links = [link.Link]
|
|
"A list containing a self link and associated kubernetes rootca host "
|
|
"update links"
|
|
|
|
def __init__(self, **kwargs):
|
|
self.fields = list(objects.kube_rootca_host_update.fields.keys())
|
|
for k in self.fields:
|
|
if not hasattr(self, k):
|
|
continue
|
|
setattr(self, k, kwargs.get(k, wtypes.Unset))
|
|
|
|
@classmethod
|
|
def convert_with_links(cls, kube_rootca_host_update, expand=True):
|
|
kube_rootca_host_update = KubeRootCAHostUpdate(
|
|
**kube_rootca_host_update.as_dict())
|
|
if not expand:
|
|
kube_rootca_host_update.unset_fields_except(['uuid',
|
|
'target_rootca_cert', 'effective_rootca_cert', 'state'])
|
|
|
|
kube_rootca_host_update.links = [
|
|
link.Link.make_link('self', pecan.request.host_url,
|
|
'kube_rootca_host_update',
|
|
kube_rootca_host_update.uuid),
|
|
link.Link.make_link('bookmark',
|
|
pecan.request.host_url,
|
|
'kube_rootca_host_update',
|
|
kube_rootca_host_update.uuid,
|
|
bookmark=True)
|
|
]
|
|
return kube_rootca_host_update
|
|
|
|
|
|
class KubeRootCAHostUpdateCollection(collection.Collection):
|
|
kube_host_updates = [KubeRootCAHostUpdate]
|
|
"A list containing kubernetes rootCA update hosts"
|
|
|
|
def __init__(self):
|
|
self._type = 'kube_host_updates'
|
|
|
|
@classmethod
|
|
def convert_with_links(cls, kube_host_update_objs, limit=None, url=None,
|
|
expand=True, **kwargs):
|
|
update_collection = KubeRootCAHostUpdateCollection()
|
|
update_collection.kube_host_updates = [
|
|
KubeRootCAHostUpdate.convert_with_links(p, expand)
|
|
for p in kube_host_update_objs]
|
|
update_collection.next = update_collection.get_next(
|
|
limit, url=url, **kwargs)
|
|
return update_collection
|
|
|
|
|
|
class KubeRootCAUpdateCollection(collection.Collection):
|
|
kube_rootca_updates = [KubeRootCAUpdate]
|
|
"A list containing kubernetes rootCA updates"
|
|
|
|
def __init__(self):
|
|
self._type = 'kube_rootca_updates'
|
|
|
|
@classmethod
|
|
def convert_with_links(cls, kube_host_update_objs, limit=None, url=None,
|
|
expand=True, **kwargs):
|
|
update_collection = KubeRootCAUpdateCollection()
|
|
update_collection.kube_rootca_updates = [
|
|
KubeRootCAUpdate.convert_with_links(p, expand)
|
|
for p in kube_host_update_objs]
|
|
update_collection.next = update_collection.get_next(
|
|
limit, url=url, **kwargs)
|
|
return update_collection
|
|
|
|
|
|
class KubeRootCAPodsUpdateController(rest.RestController):
|
|
|
|
def _precheck_trustbothcas(self, cluster_update):
|
|
# Pre checking if conditions met for phase trustBothCAs
|
|
if cluster_update.state not in \
|
|
[kubernetes.KUBE_ROOTCA_UPDATED_HOST_TRUSTBOTHCAS,
|
|
kubernetes.KUBE_ROOTCA_UPDATING_PODS_TRUSTBOTHCAS_FAILED]:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-pods-update phase trust-both-cas rejected: "
|
|
"not allowed when cluster update is in state: %s. "
|
|
"(only allowed when in state: %s or %s)"
|
|
% (cluster_update.state,
|
|
kubernetes.KUBE_ROOTCA_UPDATED_HOST_TRUSTBOTHCAS,
|
|
kubernetes.KUBE_ROOTCA_UPDATING_PODS_TRUSTBOTHCAS_FAILED)))
|
|
|
|
def _precheck_trustnewca(self, cluster_update):
|
|
# Pre checking if conditions met for phase trustNewCA
|
|
if cluster_update.state not in \
|
|
[kubernetes.KUBE_ROOTCA_UPDATED_HOST_TRUSTNEWCA,
|
|
kubernetes.KUBE_ROOTCA_UPDATING_PODS_TRUSTNEWCA_FAILED]:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-pods-update phase trust-new-ca rejected: "
|
|
"not allowed when cluster update is in state: %s. "
|
|
"(only allowed when in state: %s or %s)"
|
|
% (cluster_update.state,
|
|
kubernetes.KUBE_ROOTCA_UPDATED_HOST_TRUSTNEWCA,
|
|
kubernetes.KUBE_ROOTCA_UPDATING_PODS_TRUSTNEWCA_FAILED)))
|
|
|
|
@cutils.synchronized(LOCK_KUBE_ROOTCA_CONTROLLER)
|
|
@wsme_pecan.wsexpose(KubeRootCAUpdate, body=six.text_type)
|
|
def post(self, body):
|
|
# Check cluster update status
|
|
try:
|
|
update = pecan.request.dbapi.kube_rootca_update_get_one()
|
|
except exception.NotFound:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-pods-update rejected: No update in progress."))
|
|
|
|
phase = body['phase'].lower()
|
|
|
|
if phase == constants.KUBE_CERT_UPDATE_TRUSTBOTHCAS:
|
|
# kube root CA update for pods phase trustBothCAs
|
|
self._precheck_trustbothcas(update)
|
|
update_state = kubernetes.KUBE_ROOTCA_UPDATING_PODS_TRUSTBOTHCAS
|
|
elif phase == constants.KUBE_CERT_UPDATE_TRUSTNEWCA:
|
|
# kube root CA update for pods phase trustNewCA
|
|
self._precheck_trustnewca(update)
|
|
update_state = kubernetes.KUBE_ROOTCA_UPDATING_PODS_TRUSTNEWCA
|
|
else:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-pods-update rejected: phase %s not supported."
|
|
% (phase)))
|
|
|
|
# Update the cluster update state
|
|
values = dict()
|
|
values['state'] = update_state
|
|
update = \
|
|
pecan.request.dbapi.kube_rootca_update_update(update.id, values)
|
|
|
|
# perform rpc to conductor to perform config apply
|
|
pecan.request.rpcapi.kube_certificate_update_for_pods(
|
|
pecan.request.context, phase)
|
|
|
|
return KubeRootCAUpdate.convert_with_links(update)
|
|
|
|
|
|
class KubeRootCAHostUpdateListController(rest.RestController):
|
|
|
|
@wsme_pecan.wsexpose(KubeRootCAHostUpdateCollection)
|
|
def get(self):
|
|
"""Retrieves a list of kubernetes rootca update status by host"""
|
|
|
|
try:
|
|
pecan.request.dbapi.kube_rootca_update_get_one()
|
|
except exception.NotFound:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-update-list rejected: No kubernetes root CA update in progress."))
|
|
|
|
rpc_host_update_status_list = pecan.request.dbapi.kube_rootca_host_update_get_list()
|
|
return KubeRootCAHostUpdateCollection.convert_with_links(rpc_host_update_status_list)
|
|
|
|
|
|
class KubeRootCAUpdateController(rest.RestController):
|
|
"""REST controller for kubernetes rootCA updates."""
|
|
|
|
# Controller for /kube_rootca_update/upload_cert, upload new root CA
|
|
# certificate.
|
|
upload_cert = KubeRootCAUploadController()
|
|
# Controller for /kube_rootca_update/generate_cert, generates a new root CA
|
|
generate_cert = KubeRootCAGenerateController()
|
|
# Controller for /kube_rootca_update/pods, update pods certificates.
|
|
pods = KubeRootCAPodsUpdateController()
|
|
# Controller for /kube_rootca_update/hosts, list updates by hosts.
|
|
hosts = KubeRootCAHostUpdateListController()
|
|
|
|
def __init__(self):
|
|
self.fm_api = fm_api.FaultAPIs()
|
|
self.alarm_instance_id = "%s=%s" % (fm_constants.FM_ENTITY_TYPE_HOST,
|
|
constants.CONTROLLER_HOSTNAME)
|
|
|
|
def _get_updates(self, patch):
|
|
"""Retrieve the updated attributes from the patch request."""
|
|
updates = {}
|
|
for p in patch:
|
|
attribute = p['path'] if p['path'][0] != '/' else p['path'][1:]
|
|
updates[attribute] = p['value']
|
|
return updates
|
|
|
|
def _check_cluster_health(self, command_name, alarm_ignore_list=None, force=False):
|
|
healthy, output = pecan.request.rpcapi.get_system_health(
|
|
pecan.request.context,
|
|
force=force,
|
|
kube_rootca_update=True,
|
|
alarm_ignore_list=alarm_ignore_list)
|
|
if not healthy:
|
|
LOG.info("Health query failure for %s:\n%s"
|
|
% (command_name, output))
|
|
if os.path.exists(constants.SYSINV_RUNNING_IN_LAB):
|
|
LOG.info("Running in lab, ignoring health errors.")
|
|
else:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"System is not healthy. Run system health-query for more details."))
|
|
|
|
def _clear_kubernetes_resources(self, hostnames):
|
|
"""Clears secrets and issuers created during the update process
|
|
If a resource is not found a warning is logged and the operation continues"""
|
|
|
|
secret_list = [
|
|
constants.KUBE_ADMIN_CERT,
|
|
constants.KUBE_ROOTCA_SECRET,
|
|
]
|
|
|
|
for hostname in hostnames:
|
|
secret_list.append(constants.KUBE_APISERVER_CERT.format(hostname))
|
|
secret_list.append(constants.KUBE_APISERVER_KUBELET_CERT.format(hostname))
|
|
secret_list.append(constants.KUBE_SCHEDULER_CERT.format(hostname))
|
|
secret_list.append(constants.KUBE_CONTROLLER_MANAGER_CERT.format(hostname))
|
|
secret_list.append(constants.KUBE_KUBELET_CERT.format(hostname))
|
|
|
|
certificate_list = []
|
|
# Certificates and secrets share the same name
|
|
certificate_list.extend(secret_list)
|
|
|
|
issuers_list = [
|
|
constants.KUBE_SELFSIGNED_ISSUER,
|
|
constants.KUBE_ROOTCA_ISSUER,
|
|
]
|
|
|
|
pecan.request.rpcapi.clear_kubernetes_rootca_update_resources(
|
|
context=pecan.request.context,
|
|
certificate_list=certificate_list,
|
|
issuers_list=issuers_list,
|
|
secret_list=secret_list)
|
|
|
|
@cutils.synchronized(LOCK_KUBE_ROOTCA_CONTROLLER)
|
|
@wsme_pecan.wsexpose(KubeRootCAUpdate, wtypes.text, body=six.text_type)
|
|
def post(self, force, body):
|
|
"""Create a new Kubernetes RootCA Update and start update."""
|
|
|
|
force = force == 'True'
|
|
alarm_ignore_list = body.get('alarm_ignore_list', [])
|
|
alarm_ignore_list.append(fm_constants.FM_ALARM_ID_KUBE_ROOTCA_UPDATE_ABORTED)
|
|
|
|
try:
|
|
update = pecan.request.dbapi.kube_rootca_update_get_one()
|
|
except exception.NotFound:
|
|
pass
|
|
else:
|
|
if update.state == kubernetes.KUBE_ROOTCA_UPDATE_ABORTED:
|
|
pecan.request.dbapi.kube_rootca_update_destroy(update.id)
|
|
else:
|
|
raise wsme.exc.ClientSideError((
|
|
"A kubernetes rootca update is already in progress"))
|
|
|
|
# There must not be a platform upgrade in progress
|
|
try:
|
|
pecan.request.dbapi.software_upgrade_get_one()
|
|
except exception.NotFound:
|
|
pass
|
|
else:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"A kubernetes rootca update cannot be done while a platform upgrade is in progress"))
|
|
|
|
# There must not be a kubernetes upgrade in progress
|
|
try:
|
|
pecan.request.dbapi.kube_upgrade_get_one()
|
|
except exception.NotFound:
|
|
pass
|
|
else:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"A kubernetes rootca update cannot be done while a kube upgrade "
|
|
"is in progress"))
|
|
|
|
# The system must be healthy
|
|
self._check_cluster_health(
|
|
"kube-rootca-update-start",
|
|
alarm_ignore_list=alarm_ignore_list,
|
|
force=force,
|
|
)
|
|
|
|
# Get current kubernetes root CA certificate ID
|
|
try:
|
|
from_rootca_cert = \
|
|
pecan.request.rpcapi.get_current_kube_rootca_cert_id(
|
|
context=pecan.request.context)
|
|
except Exception:
|
|
raise wsme.exc.ClientSideError(
|
|
"Failed to get the current kubernetes root CA certificate ID")
|
|
|
|
create_obj = {'state': kubernetes.KUBE_ROOTCA_UPDATE_STARTED,
|
|
'from_rootca_cert': from_rootca_cert
|
|
}
|
|
new_update = pecan.request.dbapi.kube_rootca_update_create(create_obj)
|
|
|
|
# create update entries for each of the nodes
|
|
hosts = pecan.request.dbapi.ihost_get_list()
|
|
for host in hosts:
|
|
if host.personality in [constants.CONTROLLER,
|
|
constants.WORKER]:
|
|
pecan.request.dbapi.kube_rootca_host_update_create(host.id,
|
|
{'effective_rootca_cert': from_rootca_cert})
|
|
|
|
# clear update aborted alarm if there is one
|
|
if self.fm_api.get_fault(fm_constants.FM_ALARM_ID_KUBE_ROOTCA_UPDATE_ABORTED,
|
|
self.alarm_instance_id):
|
|
self.fm_api.clear_fault(fm_constants.FM_ALARM_ID_KUBE_ROOTCA_UPDATE_ABORTED,
|
|
self.alarm_instance_id)
|
|
|
|
# raise update in progess alarm
|
|
fault = fm_api.Fault(
|
|
alarm_id=fm_constants.FM_ALARM_ID_KUBE_ROOTCA_UPDATE_IN_PROGRESS,
|
|
alarm_state=fm_constants.FM_ALARM_STATE_SET,
|
|
entity_type_id=fm_constants.FM_ENTITY_TYPE_HOST,
|
|
entity_instance_id=self.alarm_instance_id,
|
|
severity=fm_constants.FM_ALARM_SEVERITY_MINOR,
|
|
reason_text="Kubernetes rootca update in progress",
|
|
# environmental
|
|
alarm_type=fm_constants.FM_ALARM_TYPE_5,
|
|
# unspecified-reason
|
|
probable_cause=fm_constants.ALARM_PROBABLE_CAUSE_65,
|
|
proposed_repair_action="Wait for kubernetes rootca procedure to complete",
|
|
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)
|
|
def get_one(self, uuid=None):
|
|
"""Retrieve information about the given kubernetes rootca update."""
|
|
|
|
rpc_kube_rootca_update = objects.kube_rootca_update.get_by_uuid(
|
|
pecan.request.context, uuid)
|
|
return KubeRootCAUpdate.convert_with_links(rpc_kube_rootca_update)
|
|
|
|
@wsme_pecan.wsexpose(KubeRootCAUpdateCollection)
|
|
def get(self):
|
|
"""Retrieve kubernetes rootca update status."""
|
|
rpc_kube_rootca_update = pecan.request.dbapi.kube_rootca_update_get_list()
|
|
return KubeRootCAUpdateCollection.convert_with_links(rpc_kube_rootca_update)
|
|
|
|
@cutils.synchronized(LOCK_KUBE_ROOTCA_CONTROLLER)
|
|
@wsme.validate(wtypes.text, [KubeRootCAUpdatePatchType])
|
|
@wsme_pecan.wsexpose(KubeRootCAUpdate, wtypes.text, body=[KubeRootCAUpdatePatchType])
|
|
def patch(self, force, patch):
|
|
"""Completes the kubernetes rootca, clearing both tables and alarm"""
|
|
force = force == 'True'
|
|
updates = self._get_updates(patch)
|
|
update_state = updates['state']
|
|
|
|
if update_state not in [kubernetes.KUBE_ROOTCA_UPDATE_COMPLETED,
|
|
kubernetes.KUBE_ROOTCA_UPDATE_ABORTED]:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"Invalid state %s supplied" % update_state))
|
|
|
|
# Check if there is an update in progress and the current state
|
|
try:
|
|
rpc_kube_rootca_update = pecan.request.dbapi.kube_rootca_update_get_one()
|
|
if update_state == kubernetes.KUBE_ROOTCA_UPDATE_COMPLETED \
|
|
and rpc_kube_rootca_update.state != kubernetes.KUBE_ROOTCA_UPDATED_PODS_TRUSTNEWCA:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-update-complete rejected: Expect to find cluster update state %s, "
|
|
"not allowed when cluster update state is %s."
|
|
% (kubernetes.KUBE_ROOTCA_UPDATED_PODS_TRUSTNEWCA, rpc_kube_rootca_update.state)))
|
|
elif update_state == kubernetes.KUBE_ROOTCA_UPDATE_ABORTED \
|
|
and rpc_kube_rootca_update.state == kubernetes.KUBE_ROOTCA_UPDATE_ABORTED:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-update-complete rejected: The update has already been aborted."))
|
|
|
|
except exception.NotFound:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-update-complete rejected: No kubernetes root CA update in progress."))
|
|
|
|
self._check_cluster_health(
|
|
"kube-rootca-update-complete",
|
|
alarm_ignore_list=[
|
|
fm_constants.FM_ALARM_ID_KUBE_ROOTCA_UPDATE_IN_PROGRESS,
|
|
fm_constants.FM_ALARM_ID_KUBE_ROOTCA_UPDATE_AUTO_APPLY_INPROGRESS],
|
|
force=force,
|
|
)
|
|
|
|
rpc_host_update_list = pecan.request.dbapi.kube_rootca_host_update_get_list()
|
|
hostnames = [host.hostname for host in rpc_host_update_list]
|
|
|
|
LOG.debug("Starting cleanup procedure")
|
|
|
|
self._clear_kubernetes_resources(hostnames)
|
|
|
|
# cleanup kube_rootca_host_update table
|
|
pecan.request.dbapi.kube_rootca_host_update_destroy_all()
|
|
|
|
# if update is in the list of states, abort will be the same as complete
|
|
if update_state == kubernetes.KUBE_ROOTCA_UPDATE_ABORTED \
|
|
and rpc_kube_rootca_update.state in [kubernetes.KUBE_ROOTCA_UPDATED_PODS_TRUSTNEWCA]:
|
|
update_state = kubernetes.KUBE_ROOTCA_UPDATE_COMPLETED
|
|
|
|
values = dict()
|
|
values['state'] = update_state
|
|
update = \
|
|
pecan.request.dbapi.kube_rootca_update_update(rpc_kube_rootca_update.id, values)
|
|
|
|
# cleanup kube_rootca_update table for completion
|
|
if update_state == kubernetes.KUBE_ROOTCA_UPDATE_COMPLETED:
|
|
pecan.request.dbapi.kube_rootca_update_destroy(rpc_kube_rootca_update.id)
|
|
LOG.debug("Removed procedure entry on kube_rootca_update table")
|
|
|
|
# If applicable, notify dcmanager that the update is completed
|
|
system = pecan.request.dbapi.isystem_get_one()
|
|
role = system.get('distributed_cloud_role')
|
|
if role == constants.DISTRIBUTED_CLOUD_ROLE_SYSTEMCONTROLLER:
|
|
dc_api.notify_dcmanager_kube_rootca_update_completed()
|
|
LOG.info("Kubernetes rootca update completed")
|
|
|
|
# clear update in progess alarm
|
|
if self.fm_api.get_fault(fm_constants.FM_ALARM_ID_KUBE_ROOTCA_UPDATE_IN_PROGRESS,
|
|
self.alarm_instance_id):
|
|
LOG.debug("Removing KUBE ROOTCA UPDATE IN PROGRESS alarm")
|
|
self.fm_api.clear_fault(fm_constants.FM_ALARM_ID_KUBE_ROOTCA_UPDATE_IN_PROGRESS,
|
|
self.alarm_instance_id)
|
|
|
|
# raise update aborted alarm if this is an abort
|
|
if update_state == kubernetes.KUBE_ROOTCA_UPDATE_ABORTED:
|
|
LOG.debug("Raising KUBE ROOTCA PROCEDURE ABORTED alarm")
|
|
fault = fm_api.Fault(
|
|
alarm_id=fm_constants.FM_ALARM_ID_KUBE_ROOTCA_UPDATE_ABORTED,
|
|
alarm_state=fm_constants.FM_ALARM_STATE_SET,
|
|
entity_type_id=fm_constants.FM_ENTITY_TYPE_HOST,
|
|
entity_instance_id=self.alarm_instance_id,
|
|
severity=fm_constants.FM_ALARM_SEVERITY_MINOR,
|
|
reason_text="Kubernetes root CA update aborted, certificates may not be fully updated.",
|
|
# environmental
|
|
alarm_type=fm_constants.FM_ALARM_TYPE_5,
|
|
# unspecified-reason
|
|
probable_cause=fm_constants.ALARM_PROBABLE_CAUSE_65,
|
|
proposed_repair_action="Fully update certificates by a new root CA update.",
|
|
service_affecting=False)
|
|
self.fm_api.set_fault(fault)
|
|
LOG.info("Kubernetes rootca update aborted")
|
|
|
|
return KubeRootCAUpdate.convert_with_links(update)
|
|
|
|
|
|
class KubeRootCAHostUpdateController(rest.RestController):
|
|
"""REST controller for kube host root CA certificate."""
|
|
|
|
def __init__(self, from_ihosts=False):
|
|
self._from_ihosts = from_ihosts
|
|
|
|
def _precheck_updatecerts(self, cluster_update, ihost):
|
|
|
|
# Get all the host update states and ensure this phase of the
|
|
# procedure is allowed to be executed.
|
|
host_updates = pecan.request.dbapi.kube_rootca_host_update_get_list()
|
|
if len(host_updates) == 0:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase update-certs rejected: "
|
|
"host update not started yet"))
|
|
|
|
if cluster_update.state in [
|
|
kubernetes.KUBE_ROOTCA_UPDATED_HOST_UPDATECERTS,
|
|
kubernetes.KUBE_ROOTCA_UPDATING_HOST_UPDATECERTS,
|
|
kubernetes.KUBE_ROOTCA_UPDATING_HOST_UPDATECERTS_FAILED]:
|
|
if cluster_update.state == kubernetes.KUBE_ROOTCA_UPDATED_HOST_UPDATECERTS:
|
|
# updatecerts phase completed
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase update-certs rejected: "
|
|
"procedure phase already completed on cluster"))
|
|
for host_update in host_updates:
|
|
if host_update.state == kubernetes.KUBE_ROOTCA_UPDATING_HOST_UPDATECERTS_FAILED:
|
|
# other host is on FAILED state
|
|
if host_update.host_id != ihost.id:
|
|
host_name = pecan.request.dbapi.ihost_get(
|
|
host_update.host_id).hostname
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase update-certs rejected: "
|
|
"procedure phase failed on host %s" % host_name))
|
|
elif host_update.state == kubernetes.KUBE_ROOTCA_UPDATING_HOST_UPDATECERTS:
|
|
# procedure already in progress in some host
|
|
host_name = pecan.request.dbapi.ihost_get(
|
|
host_update.host_id).hostname
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase update-certs rejected: "
|
|
"procedure phase in progress on host %s" % host_name))
|
|
elif host_update.state == kubernetes.KUBE_ROOTCA_UPDATED_HOST_UPDATECERTS:
|
|
if host_update.host_id == ihost.id:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase update-certs rejected: "
|
|
"procedure phase already completed on host %s"
|
|
% ihost.hostname))
|
|
|
|
# if is the first host to be update on this phase we need
|
|
# to assure that cluster_update state is what we expect
|
|
# that is the state where all pods were restarted and running successfully
|
|
else:
|
|
if cluster_update.state != kubernetes.KUBE_ROOTCA_UPDATED_PODS_TRUSTBOTHCAS:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase update-certs rejected: "
|
|
"procedure phase not allowed when cluster update is "
|
|
"in state: %s (only allowed when in state: %s)."
|
|
% (cluster_update.state,
|
|
kubernetes.KUBE_ROOTCA_UPDATED_PODS_TRUSTBOTHCAS)))
|
|
|
|
def _precheck_trustbothcas(self, cluster_update, ihost):
|
|
""" Pre checking if conditions met for phase trust-both-cas """
|
|
|
|
# Get all the host update state
|
|
host_updates_temp = []
|
|
host_updates_temp = \
|
|
pecan.request.dbapi.kube_rootca_host_update_get_list()
|
|
host_updates = [host_update for host_update in host_updates_temp
|
|
if host_update.state is not None]
|
|
|
|
if len(host_updates) == 0:
|
|
# No hosts start update yet
|
|
if cluster_update.state not in \
|
|
[kubernetes.KUBE_ROOTCA_UPDATE_CERT_UPLOADED,
|
|
kubernetes.KUBE_ROOTCA_UPDATE_CERT_GENERATED]:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase trust-both-cas rejected: "
|
|
"No new certificate available."))
|
|
else:
|
|
# Not allowed if any host updates are in progress
|
|
for host_update in host_updates:
|
|
if host_update.state == \
|
|
kubernetes.KUBE_ROOTCA_UPDATING_HOST_TRUSTBOTHCAS:
|
|
host_name = pecan.request.dbapi.ihost_get(
|
|
host_update.host_id).hostname
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase trust-both-cas rejected: "
|
|
"procedure phase in progress on host %s" % host_name))
|
|
|
|
# Check if this host update ever started
|
|
for host_update in host_updates:
|
|
if ihost.id == host_update.host_id:
|
|
update_ever_started = host_update
|
|
break
|
|
else:
|
|
update_ever_started = None
|
|
|
|
if update_ever_started is None:
|
|
# Update never started on this host.
|
|
# Allow start only if overall update state is correct.
|
|
if cluster_update.state not in \
|
|
[kubernetes.KUBE_ROOTCA_UPDATING_HOST_TRUSTBOTHCAS]:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase trust-both-cas rejected: "
|
|
"procedure phase not allowed when cluster update is in "
|
|
"state: %s (only allowed when in state: %s)."
|
|
% (cluster_update.state,
|
|
kubernetes.KUBE_ROOTCA_UPDATING_HOST_TRUSTBOTHCAS)))
|
|
else:
|
|
# Update ever started on this host.
|
|
if update_ever_started.state in \
|
|
[kubernetes.
|
|
KUBE_ROOTCA_UPDATING_HOST_TRUSTBOTHCAS_FAILED]:
|
|
# Allowed retry only if update on this host ever failed
|
|
pass
|
|
elif update_ever_started.state in \
|
|
[kubernetes.KUBE_ROOTCA_UPDATED_HOST_TRUSTBOTHCAS]:
|
|
# Return error indicating update on this host already
|
|
# completed.
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase trust-both-cas rejected: "
|
|
"procedure phase already completed on host %s"
|
|
% ihost.hostname))
|
|
else:
|
|
# This could be the case where the cluster update already
|
|
# passes trust-both-cas (eg. in updateCerts phase), but
|
|
# client tries to make an update call of phase
|
|
# trust-both-cas.
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase trust-both-cas rejected: "
|
|
"procedure phase not allowed when cluster update is in "
|
|
"state: %s (only allowed when in state: %s)."
|
|
% (cluster_update.state,
|
|
kubernetes.KUBE_ROOTCA_UPDATING_HOST_TRUSTBOTHCAS)))
|
|
|
|
def _precheck_trustnewca(self, cluster_update, ihost):
|
|
|
|
# Get all the host update state
|
|
host_updates = pecan.request.dbapi.kube_rootca_host_update_get_list()
|
|
if len(host_updates) == 0:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase trust-new-ca "
|
|
"rejected: host update not started yet"))
|
|
|
|
if cluster_update.state in [
|
|
kubernetes.KUBE_ROOTCA_UPDATED_HOST_TRUSTNEWCA,
|
|
kubernetes.KUBE_ROOTCA_UPDATING_HOST_TRUSTNEWCA,
|
|
kubernetes.KUBE_ROOTCA_UPDATING_HOST_TRUSTNEWCA_FAILED]:
|
|
if cluster_update.state == \
|
|
kubernetes.KUBE_ROOTCA_UPDATED_HOST_TRUSTNEWCA:
|
|
# trustNewCA phase completed
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase trust-new-ca "
|
|
"rejected: procedure phase already completed "
|
|
"on cluster"))
|
|
for host_update in host_updates:
|
|
if host_update.state == kubernetes.\
|
|
KUBE_ROOTCA_UPDATING_HOST_TRUSTNEWCA_FAILED:
|
|
# other host is on FAILED state
|
|
if host_update.host_id != ihost.id:
|
|
host_name = pecan.request.dbapi.ihost_get(
|
|
host_update.host_id).hostname
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase trust-new-ca "
|
|
"rejected: procedure phase failed on host %s"
|
|
% host_name))
|
|
elif host_update.state == \
|
|
kubernetes.KUBE_ROOTCA_UPDATING_HOST_TRUSTNEWCA:
|
|
# procedure already in progress in some host
|
|
host_name = pecan.request.dbapi.ihost_get(
|
|
host_update.host_id).hostname
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase trust-new-ca "
|
|
"rejected: procedure phase in progress on "
|
|
"host %s" % host_name))
|
|
elif host_update.state == \
|
|
kubernetes.KUBE_ROOTCA_UPDATED_HOST_TRUSTNEWCA:
|
|
if host_update.host_id == ihost.id:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase trust-new-ca "
|
|
"rejected: procedure phase already "
|
|
"completed on host %s" % ihost.hostname))
|
|
|
|
# If this is the first host to be update on this phase we need to ensure
|
|
# that cluster_update state is what we expect, which is updateCerts
|
|
# phase has completed successfully on all hosts.
|
|
else:
|
|
if cluster_update.state != \
|
|
kubernetes.KUBE_ROOTCA_UPDATED_HOST_UPDATECERTS:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase trust-new-ca "
|
|
"rejected: not allowed when cluster update "
|
|
"is in state: %s (only allowed when "
|
|
"in state: %s)." % (cluster_update.state,
|
|
kubernetes.KUBE_ROOTCA_UPDATED_HOST_UPDATECERTS)))
|
|
|
|
@cutils.synchronized(LOCK_KUBE_ROOTCA_CONTROLLER)
|
|
@wsme_pecan.wsexpose(KubeRootCAHostUpdate, types.uuid, body=six.text_type)
|
|
def post(self, host_uuid, body):
|
|
"""Update the kubernetes root CA certificate on this host"""
|
|
|
|
phase = body['phase'].lower()
|
|
|
|
# Check cluster update status
|
|
try:
|
|
update = pecan.request.dbapi.kube_rootca_update_get_one()
|
|
except exception.NotFound:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase %s rejected: "
|
|
"No update in progress." % phase))
|
|
|
|
# Check if the new root CA cert secret exists, in case the secret
|
|
# is deleted unexpectly.
|
|
kube_operator = kubernetes.KubeOperator()
|
|
try:
|
|
cert_secret = kube_operator.kube_get_secret(
|
|
constants.KUBE_ROOTCA_SECRET,
|
|
kubernetes.NAMESPACE_DEPLOYMENT,
|
|
)
|
|
except Exception:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase %s rejected: failed "
|
|
"to get new root CA cert secret from kubernetes."
|
|
% phase))
|
|
|
|
if cert_secret is None:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update phase %s rejected: no new "
|
|
"root CA cert found." % phase))
|
|
|
|
try:
|
|
ihost = pecan.request.dbapi.ihost_get(host_uuid)
|
|
except exception.ServerNotFound:
|
|
raise exception.SysinvException(_(
|
|
"Invalid host_uuid %s" % host_uuid))
|
|
|
|
if ihost.personality not in [constants.CONTROLLER, constants.WORKER]:
|
|
raise exception.SysinvException(_(
|
|
"Invalid personality %s to update kube certificate." %
|
|
ihost.personality))
|
|
|
|
if phase == constants.KUBE_CERT_UPDATE_TRUSTBOTHCAS:
|
|
# kube root CA update on host phase trust-both-cas
|
|
self._precheck_trustbothcas(update, ihost)
|
|
update_state = kubernetes.KUBE_ROOTCA_UPDATING_HOST_TRUSTBOTHCAS
|
|
elif phase == constants.KUBE_CERT_UPDATE_UPDATECERTS:
|
|
# kube root CA update on host phase updateCerts
|
|
self._precheck_updatecerts(update, ihost)
|
|
update_state = kubernetes.KUBE_ROOTCA_UPDATING_HOST_UPDATECERTS
|
|
elif phase == constants.KUBE_CERT_UPDATE_TRUSTNEWCA:
|
|
# kube root CA update on host phase trustNewCA
|
|
self._precheck_trustnewca(update, ihost)
|
|
update_state = kubernetes.KUBE_ROOTCA_UPDATING_HOST_TRUSTNEWCA
|
|
else:
|
|
raise wsme.exc.ClientSideError(_(
|
|
"kube-rootca-host-update rejected: not supported phase."))
|
|
|
|
# Update the cluster update state
|
|
c_values = dict()
|
|
c_values['state'] = update_state
|
|
c_update = pecan.request.dbapi.kube_rootca_update_get_one()
|
|
pecan.request.dbapi.kube_rootca_update_update(c_update.id, c_values)
|
|
|
|
# Create or update "update state" on this host
|
|
h_values = dict()
|
|
h_values['state'] = update_state
|
|
h_values['effective_rootca_cert'] = c_update.from_rootca_cert
|
|
h_values['target_rootca_cert'] = c_update.to_rootca_cert
|
|
try:
|
|
h_update = pecan.request.dbapi.kube_rootca_host_update_get_by_host(
|
|
ihost.id)
|
|
h_update = pecan.request.dbapi.kube_rootca_host_update_update(
|
|
h_update.id, h_values)
|
|
except exception.NotFound:
|
|
h_update = pecan.request.dbapi.kube_rootca_host_update_create(
|
|
ihost.id, h_values)
|
|
|
|
if phase not in [constants.KUBE_CERT_UPDATE_TRUSTBOTHCAS,
|
|
constants.KUBE_CERT_UPDATE_UPDATECERTS,
|
|
constants.KUBE_CERT_UPDATE_TRUSTNEWCA]:
|
|
raise exception.SysinvException(_(
|
|
"Invalid phase %s to update kube certificate." %
|
|
phase))
|
|
|
|
# perform rpc to conductor to perform config apply
|
|
pecan.request.rpcapi.kube_certificate_update_by_host(
|
|
pecan.request.context, ihost, body['phase'])
|
|
|
|
LOG.info("Kubernetes rootca update started on host: %s"
|
|
% ihost.hostname)
|
|
|
|
return KubeRootCAHostUpdate.convert_with_links(h_update)
|