config/sysinv/sysinv/sysinv/sysinv/helm/helm.py

630 lines
23 KiB
Python

#
# Copyright (c) 2018 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
""" System Inventory Helm Overrides Operator."""
from __future__ import absolute_import
import eventlet
import os
import subprocess
import tempfile
import yaml
from six import iteritems
from stevedore import extension
from sysinv.common import constants
from sysinv.common import exception
from sysinv.openstack.common import log as logging
from sysinv.helm import common
LOG = logging.getLogger(__name__)
def helm_context(func):
"""Decorate to initialize the local threading context"""
def _wrapper(self, *args, **kwargs):
thread_context = eventlet.greenthread.getcurrent()
setattr(thread_context, '_helm_context', dict())
return func(self, *args, **kwargs)
return _wrapper
class HelmOperator(object):
"""Class to encapsulate helm override operations for System Inventory"""
def __init__(self, dbapi=None, path=None, docker_repository=None):
if path is None:
path = common.HELM_OVERRIDES_PATH
# Set the primary source of docker images
if docker_repository is None:
# During initial development, use upstream OSH images by default and
# switch to the STX repo when the images are validated and ready for
# use.
docker_repository = common.DOCKER_SRC_OSH
else:
valid_docker_repositories = common.DOCKER_SRCS.keys()
if docker_repository not in valid_docker_repositories:
raise exception.InvalidHelmDockerImageSource(
source=docker_repository,
valid_srcs=valid_docker_repositories)
self.dbapi = dbapi
self.path = path
self.docker_repo_source = docker_repository
# register chart operators for lookup
self.chart_operators = {}
helm_plugins = extension.ExtensionManager(
namespace='systemconfig.helm_plugins',
invoke_on_load=True, invoke_args=(self,))
for plugin in helm_plugins.extensions:
self.chart_operators.update({plugin.name: plugin.obj})
LOG.debug("Loaded helm plugin %s" % plugin.name)
# build the list of registered supported charts
self.implemented_charts = []
for chart in constants.SUPPORTED_HELM_CHARTS:
if chart in self.chart_operators.keys():
self.implemented_charts.append(chart)
@property
def context(self):
thread_context = eventlet.greenthread.getcurrent()
return getattr(thread_context, '_helm_context')
def get_helm_chart_namespaces(self, chart_name):
"""Get supported chart namespaces.
This method retrieves the namespace supported by a given chart.
:param chart_name: name of the chart
:returns: list of supported namespaces that associated overrides may be
provided.
"""
namespaces = []
if chart_name in self.implemented_charts:
namespaces = self.chart_operators[chart_name].get_namespaces()
return namespaces
@helm_context
def get_helm_chart_overrides(self, chart_name, cnamespace=None):
return self._get_helm_chart_overrides(chart_name, cnamespace)
def _get_helm_chart_overrides(self, chart_name, cnamespace=None):
"""Get the overrides for a supported chart.
This method retrieves overrides for a supported chart. Overrides for
all supported namespaces will be returned unless a specific namespace
is requested.
:param chart_name: name of a supported chart
:param cnamespace: (optional) namespace
:returns: dict of overrides.
Example Without a cnamespace parameter:
{
'kube-system': {
'deployment': {
'mode': 'cluster',
'type': 'DaemonSet'
},
},
'openstack': {
'pod': {
'replicas': {
'server': 1
}
}
}
}
Example with a cnamespace parameter: cnamespace='kube-system'
{
'deployment': {
'mode': 'cluster',
'type': 'DaemonSet'
}
}
"""
overrides = {}
if chart_name in self.implemented_charts:
try:
overrides.update(
self.chart_operators[chart_name].get_overrides(
cnamespace))
except exception.InvalidHelmNamespace:
raise
return overrides
def get_helm_application_namespaces(self, app_name):
"""Get supported application namespaces.
This method retrieves a dict of charts and their supported namespaces
for an application.
:param app_name: name of the bundle of charts required to support an
application
:returns: dict of charts and supported namespaces that associated
overrides may be provided.
"""
app_namespaces = {}
if app_name in constants.SUPPORTED_HELM_APP_NAMES:
for chart_name in constants.SUPPORTED_HELM_APP_CHARTS[app_name]:
if chart_name in self.implemented_charts:
try:
app_namespaces.update({chart_name:
self.get_helm_chart_namespaces(
chart_name)})
except exception.InvalidHelmNamespace as e:
LOG.info(e)
return app_namespaces
@helm_context
def get_helm_application_overrides(self, app_name, cnamespace=None):
return self._get_helm_application_overrides(app_name, cnamespace)
def _get_helm_application_overrides(self, app_name, cnamespace=None):
"""Get the overrides for a supported set of charts.
This method retrieves overrides for a set of supported charts that
comprise an application. Overrides for all charts and all supported
namespaces will be returned unless a specific namespace is requested.
If a specific namespace is requested, then only charts that support
that specified namespace will be returned.
:param app_name: name of a supported application (set of charts)
:param cnamespace: (optional) namespace
:returns: dict of overrides.
Example:
{
'ingress': {
'kube-system': {
'deployment': {
'mode': 'cluster',
'type': 'DaemonSet'
},
},
'openstack': {
'pod': {
'replicas': {
'server': 1
}
}
}
},
'glance': {
'openstack': {
'pod': {
'replicas': {
'server': 1
}
}
}
}
}
"""
overrides = {}
if app_name in constants.SUPPORTED_HELM_APP_NAMES:
for chart_name in constants.SUPPORTED_HELM_APP_CHARTS[app_name]:
if chart_name in self.implemented_charts:
try:
overrides.update({chart_name:
self._get_helm_chart_overrides(
chart_name,
cnamespace)})
except exception.InvalidHelmNamespace as e:
LOG.info(e)
return overrides
def _get_helm_chart_location(self, chart_name):
"""Get supported chart location.
This method returns the download location for a given chart.
:param chart_name: name of the chart
:returns: a URL as location or None if the chart is not supported
"""
if chart_name in self.implemented_charts:
return self.chart_operators[chart_name].get_chart_location(
chart_name)
return None
def _add_armada_override_header(self, chart_name, namespace, overrides):
use_chart_name_only = [common.HELM_NS_HELM_TOOLKIT]
if namespace in use_chart_name_only:
name = chart_name
else:
name = namespace + '-' + chart_name
new_overrides = {
'schema': 'armada/Chart/v1',
'metadata': {
'schema': 'metadata/Document/v1',
'name': name
},
'data': {
'values': overrides
}
}
location = self._get_helm_chart_location(chart_name)
if location:
new_overrides['data'].update({
'source': {
'location': location
}
})
return new_overrides
def merge_overrides(self, file_overrides=[], set_overrides=[]):
""" Merge helm overrides together.
:param values: A dict of different types of user override values,
'files' (which generally specify many overrides) and
'set' (which generally specify one override).
"""
# At this point we have potentially two separate types of overrides
# specified by system or user, values from files and values passed in
# via --set . We need to ensure that we call helm using the same
# mechanisms to ensure the same behaviour.
cmd = ['helm', 'install', '--dry-run', '--debug']
# Process the newly-passed-in override values
tmpfiles = []
for value_file in file_overrides:
# For values passed in from files, write them back out to
# temporary files.
tmpfile = tempfile.NamedTemporaryFile(delete=False)
tmpfile.write(value_file)
tmpfile.close()
tmpfiles.append(tmpfile.name)
cmd.extend(['--values', tmpfile.name])
for value_set in set_overrides:
cmd.extend(['--set', value_set])
env = os.environ.copy()
env['KUBECONFIG'] = '/etc/kubernetes/admin.conf'
# Make a temporary directory with a fake chart in it
try:
tmpdir = tempfile.mkdtemp()
chartfile = tmpdir + '/Chart.yaml'
with open(chartfile, 'w') as tmpchart:
tmpchart.write('name: mychart\napiVersion: v1\n'
'version: 0.1.0\n')
cmd.append(tmpdir)
# Apply changes by calling out to helm to do values merge
# using a dummy chart.
output = subprocess.check_output(cmd, env=env)
# Check output for failure
# Extract the info we want.
values = output.split('USER-SUPPLIED VALUES:\n')[1].split(
'\nCOMPUTED VALUES:')[0]
except Exception:
raise
finally:
os.remove(chartfile)
os.rmdir(tmpdir)
for tmpfile in tmpfiles:
os.remove(tmpfile)
return values
@helm_context
def generate_helm_chart_overrides(self, chart_name, cnamespace=None):
"""Generate system helm chart overrides
This method will generate system helm chart override an write them to a
yaml file.for use with the helm command. If the namespace is provided
only the overrides file for that specified namespace will be written.
:param chart_name: name of a supported chart
:param cnamespace: (optional) namespace
"""
if chart_name in self.implemented_charts:
namespaces = self.chart_operators[chart_name].get_namespaces()
if cnamespace and cnamespace not in namespaces:
LOG.exception("The %s chart does not support namespace: %s" %
(chart_name, cnamespace))
return
try:
overrides = self._get_helm_chart_overrides(
chart_name,
cnamespace)
self._write_chart_overrides(chart_name,
cnamespace,
overrides)
except Exception as e:
LOG.exception("failed to create chart overrides for %s: %s" %
(chart_name, e))
elif chart_name:
LOG.exception("%s chart is not supported" % chart_name)
else:
LOG.exception("chart name is required")
@helm_context
def generate_meta_overrides(self, chart_name, chart_namespace):
overrides = {}
if chart_name in self.implemented_charts:
try:
overrides.update(
self.chart_operators[chart_name].get_meta_overrides(
chart_namespace))
except exception.InvalidHelmNamespace:
raise
return overrides
@helm_context
def generate_helm_application_overrides(self, app_name, cnamespace=None,
armada_format=False,
combined=False):
"""Create the system overrides files for a supported application
This method will generate system helm chart overrides yaml files for a
set of supported charts that comprise an application.. If the namespace
is provided only the overrides files for that specified namespace will
be written..
:param app_name: name of the bundle of charts required to support an
application
:param cnamespace: (optional) namespace
:param armada_format: (optional) whether to emit in armada format
instead of helm format (with extra header)
:param combined: (optional) whether to apply user overrides on top of
system overrides
"""
if app_name in constants.SUPPORTED_HELM_APP_NAMES:
app_overrides = self._get_helm_application_overrides(app_name,
cnamespace)
for (chart_name, overrides) in iteritems(app_overrides):
if combined:
# The overrides at this point is the system overrides. For charts
# with multiple namespaces, the overrides would contain multiple keys,
# one for each namespace.
#
# Retrieve the user overrides of each namespace from the database
# and merge this list of user overrides if exists with the
# system overrides. Both system and user overrides contents
# are then merged based on the namespace, prepended with required
# header and written to corresponding files (<namespace>-<chart>.yaml).
file_overrides = []
for chart_namespace in overrides.keys():
try:
db_chart = self.dbapi.helm_override_get(
chart_name, chart_namespace)
db_user_overrides = db_chart.user_overrides
if db_user_overrides:
file_overrides.append(yaml.dump(
{chart_namespace: yaml.load(db_user_overrides)}))
except exception.HelmOverrideNotFound:
pass
if file_overrides:
# Use dump() instead of safe_dump() as the latter is
# not agreeable with password regex in some overrides
system_overrides = yaml.dump(overrides)
file_overrides.insert(0, system_overrides)
combined_overrides = self.merge_overrides(
file_overrides=file_overrides)
overrides = yaml.load(combined_overrides)
# If armada formatting is wanted, we need to change the
# structure of the yaml file somewhat
if armada_format:
for key in overrides:
new_overrides = self._add_armada_override_header(
chart_name, key, overrides[key])
overrides[key] = new_overrides
self._write_chart_overrides(chart_name, cnamespace, overrides)
# Write any meta-overrides for this chart. These will be in
# armada format already.
if armada_format:
overrides = self.generate_meta_overrides(chart_name,
cnamespace)
if overrides:
chart_meta_name = chart_name + '-meta'
self._write_chart_overrides(
chart_meta_name, cnamespace, overrides)
elif app_name:
LOG.exception("%s application is not supported" % app_name)
else:
LOG.exception("application name is required")
def remove_helm_chart_overrides(self, chart_name, cnamespace=None):
"""Remove the overrides files for a chart"""
if chart_name in self.implemented_charts:
namespaces = self.chart_operators[chart_name].get_namespaces()
filenames = []
if cnamespace and cnamespace in namespaces:
filenames.append("%s-%s.yaml" % (cnamespace, chart_name))
else:
for n in namespaces:
filenames.append("%s-%s.yaml" % (n, chart_name))
for f in filenames:
try:
self._remove_overrides(f)
except Exception as e:
LOG.exception("failed to remove %s overrides: %s: %s" % (
chart_name, f, e))
else:
LOG.exception("chart %s not supported for system overrides" %
chart_name)
def _write_chart_overrides(self, chart_name, cnamespace, overrides):
"""Write a one or more overrides files for a chart. """
def _write_file(filename, values):
try:
self._write_overrides(filename, values)
except Exception as e:
LOG.exception("failed to write %s overrides: %s: %s" % (
chart_name, filename, e))
if cnamespace:
_write_file("%s-%s.yaml" % (cnamespace, chart_name), overrides)
else:
for ns in overrides.keys():
_write_file("%s-%s.yaml" % (ns, chart_name), overrides[ns])
def _write_overrides(self, filename, overrides):
"""Write a single overrides file. """
filepath = os.path.join(self.path, filename)
try:
fd, tmppath = tempfile.mkstemp(dir=self.path, prefix=filename,
text=True)
with open(tmppath, 'w') as f:
yaml.dump(overrides, f, default_flow_style=False)
os.close(fd)
os.rename(tmppath, filepath)
except Exception:
LOG.exception("failed to write overrides file: %s" % filepath)
raise
def _remove_overrides(self, filename):
"""Remove a single overrides file. """
filepath = os.path.join(self.path, filename)
try:
if os.path.exists(filepath):
os.unlink(filepath)
except Exception:
LOG.exception("failed to delete overrides file: %s" % filepath)
raise
class HelmOperatorData(HelmOperator):
"""Class to allow retrieval of helm managed data"""
@helm_context
def get_keystone_auth_data(self):
keystone_operator = self.chart_operators[constants.HELM_CHART_KEYSTONE]
auth_data = {
'admin_user_name':
keystone_operator.get_admin_user_name(),
'admin_project_name':
keystone_operator.get_admin_project_name(),
'auth_host':
'keystone-api.openstack.svc.cluster.local',
'admin_user_domain':
keystone_operator.get_admin_user_domain(),
'admin_project_domain':
keystone_operator.get_admin_project_domain(),
}
return auth_data
@helm_context
def get_nova_endpoint_data(self):
nova_operator = self.chart_operators[constants.HELM_CHART_NOVA]
endpoint_data = {
'endpoint_override':
'http://nova-api.openstack.svc.cluster.local:8774',
'region_name':
nova_operator.get_region_name(),
}
return endpoint_data
@helm_context
def get_nova_oslo_messaging_data(self):
nova_operator = self.chart_operators[constants.HELM_CHART_NOVA]
endpoints_overrides = nova_operator._get_endpoints_overrides()
auth_data = {
'host':
'rabbitmq.openstack.svc.cluster.local',
'port':
5672,
'virt_host':
'nova',
'username':
endpoints_overrides['oslo_messaging']['auth']['nova']
['username'],
'password':
endpoints_overrides['oslo_messaging']['auth']['nova']
['password'],
}
return auth_data
@helm_context
def get_cinder_endpoint_data(self):
cinder_operator = self.chart_operators[constants.HELM_CHART_CINDER]
endpoint_data = {
'region_name':
cinder_operator.get_region_name(),
'service_name':
cinder_operator.get_service_name_v2(),
'service_type':
cinder_operator.get_service_type_v2(),
}
return endpoint_data
@helm_context
def get_glance_endpoint_data(self):
glance_operator = self.chart_operators[constants.HELM_CHART_GLANCE]
endpoint_data = {
'region_name':
glance_operator.get_region_name(),
'service_name':
glance_operator.get_service_name(),
'service_type':
glance_operator.get_service_type(),
}
return endpoint_data
@helm_context
def get_neutron_endpoint_data(self):
neutron_operator = self.chart_operators[constants.HELM_CHART_NEUTRON]
endpoint_data = {
'region_name':
neutron_operator.get_region_name(),
}
return endpoint_data
@helm_context
def get_heat_endpoint_data(self):
heat_operator = self.chart_operators[constants.HELM_CHART_HEAT]
endpoint_data = {
'region_name':
heat_operator.get_region_name(),
}
return endpoint_data
@helm_context
def get_ceilometer_endpoint_data(self):
ceilometer_operator = \
self.chart_operators[constants.HELM_CHART_CEILOMETER]
endpoint_data = {
'region_name':
ceilometer_operator.get_region_name(),
}
return endpoint_data