# # Copyright (c) 2018-2021 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 re import tempfile import yaml from six import iteritems from stevedore import extension from oslo_log import log as logging from sysinv.common import constants from sysinv.common import exception from sysinv.common import utils from sysinv.helm import common from sysinv.helm import utils as helm_utils LOG = logging.getLogger(__name__) # Disable yaml feature 'alias' for clean and readable output yaml.Dumper.ignore_aliases = lambda *data: True # Number of characters to strip off from helm plugin name defined in setup.cfg, # in order to allow controlling the order of the helm plugins, without changing # the names of the plugins. # The convention here is for the helm plugins to be named ###_PLUGINNAME. HELM_PLUGIN_PREFIX_LENGTH = 4 # Number of optional characters appended to Armada manifest operator name, # to allow overriding with a newer version of the Armada manifest operator. # The convention here is for the Armada operator plugins to allow an # optional suffix, as in PLUGINNAME_###. ARMADA_PLUGIN_SUFFIX_LENGTH = 4 # Number of optional characters appended to AppLifecycle operator name, # to allow overriding with a newer version of the AppLifecycle operator. # The convention here is for the AppLifecycle operator plugins to allow an # optional suffix, as in PLUGINNAME_###. LIFECYCLE_PLUGIN_SUFFIX_LENGTH = 4 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 def suppress_stevedore_errors(manager, entrypoint, exception): """ stevedore.ExtensionManager will try to import the entry point defined in the module. For helm_applications, both stx_openstack and platform_integ_apps are virtual modules. So ExtensionManager will throw the "Could not load ..." error message, which is expected. Just suppress this error message to avoid cause confusion. """ pass LOCK_NAME = 'HelmOperator' class HelmOperator(object): """Class to encapsulate helm override operations for System Inventory""" # Define the stevedore namespaces that will need to be managed for plugins STEVEDORE_APPS = 'systemconfig.helm_applications' STEVEDORE_ARMADA = 'systemconfig.armada.manifest_ops' STEVEDORE_LIFECYCLE = 'systemconfig.app_lifecycle' def __init__(self, dbapi=None): self.dbapi = dbapi # Find all plugins for apps, charts per app, and armada manifest # operators self.discover_plugins() @utils.synchronized(LOCK_NAME) def discover_plugins(self): """ Scan for all available plugins """ LOG.debug("HelmOperator: Loading available helm, armada and lifecycle plugins.") # Initialize the plugins self.helm_system_applications = {} self.chart_operators = {} self.armada_manifest_operators = {} self.app_lifecycle_operators = {} # Need to purge the stevedore plugin cache so that when we discover the # plugins, new plugin resources are found. If the cache exists, then no # new plugins are discoverable. self.purge_cache() # dict containing sequence of helm charts per app self.helm_system_applications = self._load_helm_applications() # dict containing Armada manifest operators per app self.armada_manifest_operators = self._load_armada_manifest_operators() # dict containing app lifecycle operators per app self.app_lifecycle_operators = self._load_app_lifecycle_operators() @utils.synchronized(LOCK_NAME) def purge_cache_by_location(self, install_location): """Purge the stevedore entry point cache.""" for lifecycle_ep in extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_LIFECYCLE]: lifecycle_distribution = utils.get_distribution_from_entry_point(lifecycle_ep) (project_name, project_location) = \ utils.get_project_name_and_location_from_distribution(lifecycle_distribution) if project_location == install_location: extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_LIFECYCLE].remove(lifecycle_ep) break else: LOG.info("Couldn't find endpoint distribution located at %s for " "%s" % (install_location, lifecycle_distribution)) for armada_ep in extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_ARMADA]: armada_distribution = utils.get_distribution_from_entry_point(armada_ep) (project_name, project_location) = \ utils.get_project_name_and_location_from_distribution(armada_distribution) if project_location == install_location: extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_ARMADA].remove(armada_ep) break else: LOG.info("Couldn't find endpoint distribution located at %s for " "%s" % (install_location, armada_distribution)) for app_ep in extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_APPS]: app_distribution = utils.get_distribution_from_entry_point(app_ep) (app_project_name, app_project_location) = \ utils.get_project_name_and_location_from_distribution(app_distribution) if app_project_location == install_location: namespace = utils.get_module_name_from_entry_point(app_ep) purged_list = [] for helm_ep in extension.ExtensionManager.ENTRY_POINT_CACHE[namespace]: helm_distribution = utils.get_distribution_from_entry_point(helm_ep) (helm_project_name, helm_project_location) = \ utils.get_project_name_and_location_from_distribution(helm_distribution) if helm_project_location != install_location: purged_list.append(helm_ep) if purged_list: extension.ExtensionManager.ENTRY_POINT_CACHE[namespace] = purged_list else: del extension.ExtensionManager.ENTRY_POINT_CACHE[namespace] extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_APPS].remove(app_ep) LOG.info("Removed stevedore namespace: %s" % namespace) def purge_cache(self): """Purge the stevedore entry point cache.""" if self.STEVEDORE_APPS in extension.ExtensionManager.ENTRY_POINT_CACHE: for entry_point in extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_APPS]: namespace = utils.get_module_name_from_entry_point(entry_point) try: del extension.ExtensionManager.ENTRY_POINT_CACHE[namespace] LOG.debug("Deleted entry points for %s." % namespace) except KeyError: LOG.info("No entry points for %s found." % namespace) try: del extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_APPS] LOG.debug("Deleted entry points for %s." % self.STEVEDORE_APPS) except KeyError: LOG.info("No entry points for %s found." % self.STEVEDORE_APPS) else: LOG.info("No entry points for %s found." % self.STEVEDORE_APPS) try: del extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_ARMADA] LOG.debug("Deleted entry points for %s." % self.STEVEDORE_ARMADA) except KeyError: LOG.info("No entry points for %s found." % self.STEVEDORE_ARMADA) try: del extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_LIFECYCLE] LOG.debug("Deleted entry points for %s." % self.STEVEDORE_LIFECYCLE) except KeyError: LOG.info("No entry points for %s found." % self.STEVEDORE_LIFECYCLE) def _load_app_lifecycle_operators(self): """Build a dictionary of AppLifecycle operators""" operators_dict = {} app_lifecycle_operators = extension.ExtensionManager( namespace=self.STEVEDORE_LIFECYCLE, invoke_on_load=True, invoke_args=()) sorted_app_lifecycle_operators = sorted( app_lifecycle_operators.extensions, key=lambda x: x.name) for operator in sorted_app_lifecycle_operators: if (operator.name[-(LIFECYCLE_PLUGIN_SUFFIX_LENGTH - 1):].isdigit() and operator.name[-LIFECYCLE_PLUGIN_SUFFIX_LENGTH:-3] == '_'): operator_name = operator.name[0:-LIFECYCLE_PLUGIN_SUFFIX_LENGTH] else: operator_name = operator.name operators_dict[operator_name] = operator.obj return operators_dict def get_app_lifecycle_operator(self, app_name): """Return an AppLifecycle operator based on app name""" if app_name in self.app_lifecycle_operators: operator = self.app_lifecycle_operators[app_name] else: operator = self.app_lifecycle_operators['generic'] return operator def _load_armada_manifest_operators(self): """Build a dictionary of armada manifest operators""" operators_dict = {} dist_info_dict = {} armada_manifest_operators = extension.ExtensionManager( namespace=self.STEVEDORE_ARMADA, invoke_on_load=True, invoke_args=()) sorted_armada_manifest_operators = sorted( armada_manifest_operators.extensions, key=lambda x: x.name) for op in sorted_armada_manifest_operators: if (op.name[-(ARMADA_PLUGIN_SUFFIX_LENGTH - 1):].isdigit() and op.name[-ARMADA_PLUGIN_SUFFIX_LENGTH:-3] == '_'): op_name = op.name[0:-ARMADA_PLUGIN_SUFFIX_LENGTH] else: op_name = op.name operators_dict[op_name] = op.obj distribution = utils.get_distribution_from_entry_point(op.entry_point) (project_name, project_location) = \ utils.get_project_name_and_location_from_distribution(distribution) # Extract distribution information for logging dist_info_dict[op_name] = { 'name': project_name, 'location': project_location, } # Provide some log feedback on plugins being used for (app_name, info) in iteritems(dist_info_dict): LOG.debug("Plugins for %-20s: loaded from %-20s - %s." % (app_name, info['name'], info['location'])) return operators_dict def get_armada_manifest_operator(self, app_name): """Return a manifest operator based on app name""" if app_name in self.armada_manifest_operators: manifest_op = self.armada_manifest_operators[app_name] else: manifest_op = self.armada_manifest_operators['generic'] return manifest_op def _load_helm_applications(self): """Build a dictionary of supported helm applications""" helm_application_dict = {} helm_applications = extension.ExtensionManager( namespace=self.STEVEDORE_APPS, on_load_failure_callback=suppress_stevedore_errors ) for entry_point in helm_applications.list_entry_points(): helm_application_dict[entry_point.name] = \ utils.get_module_name_from_entry_point(entry_point) supported_helm_applications = {} for name, namespace in helm_application_dict.items(): supported_helm_applications[name] = [] helm_plugins = extension.ExtensionManager( namespace=namespace, invoke_on_load=True, invoke_args=(self,)) sorted_helm_plugins = sorted(helm_plugins.extensions, key=lambda x: x.name) for plugin in sorted_helm_plugins: distribution = utils.get_distribution_from_entry_point(plugin.entry_point) (project_name, project_location) = \ utils.get_project_name_and_location_from_distribution(distribution) LOG.debug("%s: helm plugin %s loaded from %s - %s." % (name, plugin.name, project_name, project_location)) plugin_name = plugin.name[HELM_PLUGIN_PREFIX_LENGTH:] self.chart_operators.update({plugin_name: plugin.obj}) # Remove duplicates, keeping last occurrence only if plugin_name in supported_helm_applications[name]: supported_helm_applications[name].remove(plugin_name) supported_helm_applications[name].append(plugin_name) return supported_helm_applications def get_active_helm_applications(self): """ Get the active system applications and charts """ return self.helm_system_applications @property def context(self): thread_context = eventlet.greenthread.getcurrent() return getattr(thread_context, '_helm_context') def get_helm_chart_namespaces_by_app(self, chart_name, app_name): """Get supported chart namespaces for a given application. This method retrieves the namespace supported by a given chart. :param chart_name: name of the chart :param app_name: name of the application :returns: list of supported namespaces that associated overrides may be provided. """ namespaces = [] if chart_name in self.chart_operators: namespaces = self.chart_operators[chart_name].get_namespaces_by_app( app_name) return namespaces 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.chart_operators: 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.chart_operators: 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. """ try: app = self.dbapi.kube_app_get(app_name) except exception.KubeAppNotFound: LOG.exception("Application %s not found." % app_name) raise app_namespaces = {} if app_name in self.helm_system_applications: for chart_name in self.helm_system_applications[app_name]: try: app_namespaces.update( {chart_name: self.get_helm_chart_namespaces_by_app( chart_name, app_name)}) except exception.InvalidHelmNamespace as e: LOG.info(e) else: # Generic apps db_namespaces = self.dbapi.helm_override_get_all(app.id) for chart in db_namespaces: app_namespaces.setdefault( chart.name, []).append(chart.namespace) 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 self.helm_system_applications: for chart_name in self.helm_system_applications[app_name]: 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, repo_name, chart_tarfile): """Get the chart location. This method returns the download location for a given chart. :param chart_name: name of the chart :param repo_name: name of the repo that chart uploaded to :param chart_tarfile: name of the chart tarfile :returns: a URL as location """ if repo_name is None: repo_name = common.HELM_REPO_FOR_APPS if chart_tarfile is None: # TODO: Clean up the assumption chart_tarfile = chart_name + '-0.1.0' # Set the location based on ip address since # http://controller does not resolve in armada container. sys_controller_network = self.dbapi.network_get_by_type(constants.NETWORK_TYPE_CLUSTER_HOST) sys_controller_network_addr_pool = self.dbapi.address_pool_get(sys_controller_network.pool_uuid) sc_float_ip = sys_controller_network_addr_pool.floating_address if utils.is_valid_ipv6(sc_float_ip): sc_float_ip = '[' + sc_float_ip + ']' return 'http://{}:{}/helm_charts/{}/{}.tgz'.format( sc_float_ip, utils.get_http_port(self.dbapi), repo_name, chart_tarfile) def _add_armada_override_header(self, chart_name, chart_metadata_name, repo_name, chart_tarfile, namespace, overrides): if chart_metadata_name is None: chart_metadata_name = namespace + '-' + chart_name new_overrides = { 'schema': 'armada/Chart/v1', 'metadata': { 'schema': 'metadata/Document/v1', 'name': chart_metadata_name }, 'data': { 'values': overrides } } location = self._get_helm_chart_location(chart_name, repo_name, chart_tarfile) if location: new_overrides['data'].update({ 'source': { 'location': location } }) return new_overrides def _get_chart_info_from_armada_chart(self, chart_name, chart_namespace, chart_info_list): """ Extract the metadata name of the armada chart, repo and the name of the chart tarfile from the armada manifest chart. :param chart_name: name of the chart from the (application list) :param chart_namespace: namespace of the chart :param chart_info_list: a list of chart objects containing information extracted from the armada manifest :returns: the metadata name of the chart, the supported StarlingX repository, the name of the chart tarfile or None,None,None if not present """ # Could be called without any armada_manifest info. Returning 'None' # will enable helm defaults to point to common.HELM_REPO_FOR_APPS metadata_name = None repo = None chart_tarfile = None if chart_info_list is None: return metadata_name, repo, chart_tarfile location = None for c in chart_info_list: if (c.name == chart_name and c.namespace == chart_namespace): location = c.location metadata_name = c.metadata_name break if location: match = re.search('/helm_charts/(.*)/(.*).tgz', location) if match: repo = match.group(1) chart_tarfile = match.group(2) LOG.debug("Chart %s can be found in repo: %s" % (chart_name, repo)) return metadata_name, repo, chart_tarfile def merge_overrides(self, file_overrides=None, set_overrides=None): """ 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). """ if file_overrides is None: file_overrides = [] if set_overrides is None: set_overrides = [] # 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. args = [] # 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) args.extend(['--values', tmpfile.name]) for value_set in set_overrides: keypair = list(value_set.split("=")) # request user to input with "--set key=value" or # "--set key=", for the second case, the value is assume "" # skip setting like "--set =value", "--set xxxx" if len(keypair) == 2 and keypair[0]: if keypair[1] and keypair[1].isdigit(): args.extend(['--set-string', value_set]) else: args.extend(['--set', value_set]) try: # Apply changes by calling out to helm to do values merge # using a dummy chart. output = helm_utils.install_helm_chart_with_dry_run(args) # Extract the info we want. values = output.split('USER-SUPPLIED VALUES:\n')[1].split( '\nCOMPUTED VALUES:')[0] except Exception as e: LOG.error("Failed to merge overrides %s" % e) raise for tmpfile in tmpfiles: os.remove(tmpfile) return values @helm_context def generate_helm_chart_overrides(self, path, 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.chart_operators: 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(path, 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 @utils.synchronized(LOCK_NAME) def generate_helm_application_overrides(self, path, app_name, mode=None, cnamespace=None, armada_format=False, armada_chart_info=None, 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 mode: mode to control how to apply application manifest :param cnamespace: (optional) namespace :param armada_format: (optional) whether to emit in armada format instead of helm format (with extra header) :param armada_chart_info: (optional) supporting chart information extracted from the armada manifest which is used to influence overrides :param combined: (optional) whether to apply user overrides on top of system overrides """ try: app = self.dbapi.kube_app_get(app_name) except exception.KubeAppNotFound: LOG.exception("Application %s not found." % app_name) raise # Get a manifest operator to provide a single point of # manipulation for the chart, chart group and manifest schemas manifest_op = self.get_armada_manifest_operator(app_name) # Load the manifest into the operator armada_manifest = utils.generate_synced_armada_manifest_fqpn( app.name, app.app_version, app.manifest_file) manifest_op.load(armada_manifest) if app_name in self.helm_system_applications: 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 are 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 they # exist, with the system overrides. Both system and user # override contents are then merged based on the namespace, # prepended with required header and written to # corresponding files (-.yaml). file_overrides = [] for chart_namespace in overrides.keys(): try: db_chart = self.dbapi.helm_override_get( app.id, 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: metadata_name, repo_name, chart_tarfile = \ self._get_chart_info_from_armada_chart(chart_name, key, armada_chart_info) new_overrides = self._add_armada_override_header( chart_name, metadata_name, repo_name, chart_tarfile, key, overrides[key]) overrides[key] = new_overrides self._write_chart_overrides(path, chart_name, cnamespace, overrides) # Update manifest docs based on the plugin directives. If the # application does not provide a manifest operator, the # GenericArmadaManifestOperator is used and chart specific # operations can be skipped. if manifest_op.APP: if chart_name in self.chart_operators: self.chart_operators[chart_name].execute_manifest_updates( manifest_op) # Update the manifest based on platform conditions manifest_op.platform_mode_manifest_updates(self.dbapi, mode) else: # Generic applications for chart in armada_chart_info: try: db_chart = self.dbapi.helm_override_get( app.id, chart.name, chart.namespace) except exception.HelmOverrideNotFound: # This routine is to create helm overrides entries # in database during application-upload so that user # can list the supported helm chart overrides of the # application via helm-override-list try: values = { 'name': chart.name, 'namespace': chart.namespace, 'app_id': app.id, } db_chart = self.dbapi.helm_override_create(values=values) except Exception as e: LOG.exception(e) return user_overrides = {chart.namespace: {}} db_user_overrides = db_chart.user_overrides if db_user_overrides: user_overrides = yaml.load(yaml.dump( {chart.namespace: yaml.load(db_user_overrides)})) if armada_format: metadata_name, repo_name, chart_tarfile =\ self._get_chart_info_from_armada_chart(chart.name, chart.namespace, armada_chart_info) new_overrides = self._add_armada_override_header( chart.name, metadata_name, repo_name, chart_tarfile, chart.namespace, user_overrides[chart.namespace]) user_overrides[chart.namespace] = new_overrides self._write_chart_overrides(path, chart.name, cnamespace, user_overrides) # Write the manifest doc overrides, a summmary file for easy --value # generation on the apply, and a unified manifest for deletion. manifest_op.save_overrides() manifest_op.save_summary(path=path) manifest_op.save_delete_manifest() def remove_helm_chart_overrides(self, path, chart_name, cnamespace=None): """Remove the overrides files for a chart""" if chart_name in self.chart_operators: 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(path, 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, path, chart_name, cnamespace, overrides): """Write a one or more overrides files for a chart. """ def _write_file(filename, values): try: self._write_overrides(path, 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, path, filename, overrides): """Write a single overrides file. """ if not os.path.isdir(path): os.makedirs(path) filepath = os.path.join(path, filename) try: fd, tmppath = tempfile.mkstemp(dir=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) # Change the permission to be readable to non-root users(ie.Armada) os.chmod(filepath, 0o644) except Exception: LOG.exception("failed to write overrides file: %s" % filepath) raise def _remove_overrides(self, path, filename): """Remove a single overrides file. """ filepath = os.path.join(path, filename) try: if os.path.exists(filepath): os.unlink(filepath) except Exception: LOG.exception("failed to delete overrides file: %s" % filepath) raise @helm_context def version_check(self, app_name, app_version): """Validate application version""" if app_name in self.helm_system_applications: for chart_name in self.helm_system_applications[app_name]: if not self.chart_operators[chart_name].version_check(app_version): LOG.info("Unsupported version reported by %s: %s %s" % ( chart_name, app_name, app_version)) return False # Return True by default return True class HelmOperatorData(HelmOperator): """Class to allow retrieval of helm managed data""" # TODO (rchurch): decouple. Plugin chart names. This class needs to be # delivered as a plugin. HELM_CHART_KEYSTONE = 'keystone' HELM_CHART_NOVA = 'nova' HELM_CHART_CINDER = 'cinder' HELM_CHART_GLANCE = 'glance' HELM_CHART_NEUTRON = 'neutron' HELM_CHART_HEAT = 'heat' HELM_CHART_CEILOMETER = 'ceilometer' HELM_CHART_DCDBSYNC = 'dcdbsync' @helm_context def get_keystone_auth_data(self): keystone_operator = self.chart_operators[self.HELM_CHART_KEYSTONE] # use stx_admin account to communicate with openstack app username = common.USER_STX_ADMIN try: password = keystone_operator.get_stx_admin_password() except Exception: # old version app doesn't support stx_admin account yet. # fallback to admin account username = keystone_operator.get_admin_user_name() password = keystone_operator.get_admin_password() auth_data = { 'admin_user_name': username, 'admin_project_name': keystone_operator.get_admin_project_name(), 'auth_host': 'keystone.openstack.svc.cluster.local', 'auth_port': 80, 'admin_user_domain': keystone_operator.get_admin_user_domain(), 'admin_project_domain': keystone_operator.get_admin_project_domain(), 'admin_password': password, } return auth_data @helm_context def get_keystone_endpoint_data(self): keystone_operator = self.chart_operators[self.HELM_CHART_KEYSTONE] endpoint_data = { 'endpoint_override': 'http://keystone.openstack.svc.cluster.local:80', 'region_name': keystone_operator.get_region_name(), } return endpoint_data @helm_context def get_keystone_oslo_db_data(self): keystone_operator = self.chart_operators[self.HELM_CHART_KEYSTONE] endpoints_overrides = keystone_operator.\ _get_endpoints_oslo_db_overrides(self.HELM_CHART_KEYSTONE, ['keystone']) password = endpoints_overrides['keystone']['password'] connection = "mysql+pymysql://keystone:%s@" \ "mariadb.openstack.svc.cluster.local:3306/keystone"\ % (password) endpoint_data = { 'connection': connection, } return endpoint_data @helm_context def get_nova_endpoint_data(self): nova_operator = self.chart_operators[self.HELM_CHART_NOVA] endpoint_data = { 'endpoint_override': 'http://nova-api-internal.openstack.svc.cluster.local:80', 'region_name': nova_operator.get_region_name(), } return endpoint_data @helm_context def get_nova_oslo_messaging_data(self): nova_operator = self.chart_operators[self.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[self.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[self.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[self.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[self.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[self.HELM_CHART_CEILOMETER] endpoint_data = { 'region_name': ceilometer_operator.get_region_name(), } return endpoint_data @helm_context def get_dcdbsync_endpoint_data(self): dcdbsync_operator = self.chart_operators[self.HELM_CHART_DCDBSYNC] endpoints_overrides = dcdbsync_operator._get_endpoints_overrides() endpoint_data = { 'keystone_password': endpoints_overrides['identity']['auth']['dcdbsync'] ['password'], } return endpoint_data