# # Copyright (c) 2017-2024 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # """ System Inventory Puppet Configuration Operator.""" from __future__ import absolute_import import eventlet import io import os import tempfile import yaml import tsconfig.tsconfig as tsc from stevedore import extension from tsconfig import tsconfig from sysinv.common import constants from oslo_log import log as logging from sysinv.puppet import common from sysinv.common import utils LOG = logging.getLogger(__name__) def puppet_context(func): """Decorate to initialize the local threading context""" def _wrapper(self, *args, **kwargs): thread_context = eventlet.greenthread.getcurrent() setattr(thread_context, '_puppet_context', dict()) func(self, *args, **kwargs) return _wrapper class PuppetOperator(object): """Class to encapsulate puppet operations for System Inventory""" def __init__(self, dbapi=None, path=None): if path is None: path = common.PUPPET_HIERADATA_PATH self.dbapi = dbapi self.path = path puppet_plugins = extension.ExtensionManager( namespace='systemconfig.puppet_plugins', invoke_on_load=True, invoke_args=(self,)) self.puppet_plugins = sorted(puppet_plugins, key=lambda x: x.name) for plugin in self.puppet_plugins: plugin_name = plugin.name[4:] setattr(self, plugin_name, plugin.obj) LOG.debug("Loaded puppet plugin %s" % plugin.name) @property def context(self): thread_context = eventlet.greenthread.getcurrent() return getattr(thread_context, '_puppet_context') @property def config(self): return self.context.get('config', {}) @puppet_context def create_static_config(self): """ Create the initial static configuration that sets up one-time configuration items that are not generated by standard system configuration. This is invoked once during initial bootstrap to create the required parameters. """ # use the temporary keyring storage during bootstrap phase os.environ["XDG_DATA_HOME"] = "/tmp" try: self.context['config'] = config = {} for puppet_plugin in self.puppet_plugins: config.update(puppet_plugin.obj.get_static_config()) filename = 'static.yaml' self._write_config(filename, config) except Exception: LOG.exception("failed to create static config") raise def get_hieradata_path(self, version): if version: path = self.path.replace(tsconfig.SW_VERSION, version) else: path = self.path return path def read_static_config(self, version=None): try: filename = 'static.yaml' path = self.get_hieradata_path(version) return self._read_config(filename, path) except Exception: LOG.exception("failed to read secure_system config") raise @puppet_context def create_secure_config(self): """ Create the secure config, for storing passwords. This is invoked once during initial bootstrap to create the required parameters. """ # use the temporary keyring storage during bootstrap phase os.environ["XDG_DATA_HOME"] = "/tmp" try: self.context['config'] = config = {} for puppet_plugin in self.puppet_plugins: config.update(puppet_plugin.obj.get_secure_static_config()) filename = 'secure_static.yaml' self._write_config(filename, config) except Exception: LOG.exception("failed to create secure config") raise def read_secure_static_config(self, version=None): try: filename = 'secure_static.yaml' path = self.get_hieradata_path(version) return self._read_config(filename, path) except Exception: LOG.exception("failed to read secure_system config") raise @puppet_context def update_system_config(self): """Update the configuration for the system""" try: # NOTE: order is important due to cached context data self.context['config'] = config = {} for puppet_plugin in self.puppet_plugins: config.update(puppet_plugin.obj.get_system_config()) filename = 'system.yaml' self._write_config(filename, config) except Exception: LOG.exception("failed to create system config") raise def read_system_config(self, version=None): try: filename = 'system.yaml' path = self.get_hieradata_path(version) return self._read_config(filename, path) except Exception: LOG.exception("failed to read secure_system config") raise @puppet_context def update_secure_system_config(self): """Update the secure configuration for the system""" try: # NOTE: order is important due to cached context data self.context['config'] = config = {} for puppet_plugin in self.puppet_plugins: config.update(puppet_plugin.obj.get_secure_system_config()) filename = 'secure_system.yaml' self._write_config(filename, config) except Exception: LOG.exception("failed to create secure_system config") raise def read_secure_system_config(self, version=None): try: filename = 'secure_system.yaml' path = self.get_hieradata_path(version) return self._read_config(filename, path) except Exception: LOG.exception("failed to read secure_system config") raise def _is_controller0_downgrade(self, host, hiera_file): """ check if controller-0 will execute a downgrade for a version using mgmt_ip.yaml. for AIO-SX it is not relevant """ if (tsc.system_mode != constants.SYSTEM_MODE_SIMPLEX and host.hostname == constants.CONTROLLER_0_HOSTNAME and not os.path.exists(hiera_file)): try: upgrade = self.dbapi.software_upgrade_get_one() if (upgrade.state == constants.UPGRADE_ABORTING_ROLLBACK): LOG.info("controller-0 downgrade for a version using .yaml") return True except Exception: # upgrade not in progress pass return False @puppet_context def update_host_config(self, host, config_uuid=None): """Update the host hiera configuration files for the supplied host""" self.config_uuid = config_uuid self.context['config'] = config = {} LOG.info("Updating hiera for host: %s " "with config_uuid: %s" % (host.hostname, config_uuid)) for puppet_plugin in self.puppet_plugins: config.update(puppet_plugin.obj.get_host_config(host)) self._write_host_config(host, config) # Hiera file updated. Check if Management Network reconfiguration is ongoing if (os.path.isfile(tsc.MGMT_NETWORK_RECONFIGURATION_ONGOING) and (host.action == constants.FORCE_UNLOCK_ACTION or host.action == constants.UNLOCK_ACTION)): if not os.path.isfile(tsc.MGMT_NETWORK_RECONFIGURATION_UNLOCK): LOG.info("Management Network reconfiguration will be applied during " "the startup. Hiera files updated and host-unlock detected") open(tsc.MGMT_NETWORK_RECONFIGURATION_UNLOCK, 'w').close() def read_host_config(self, host, version=None): """""" path = self.get_hieradata_path(version) return self._read_host_config(host, path) @puppet_context def update_host_config_upgrade(self, host, target_load, config_uuid): """Update the host hiera configuration files for the supplied host and upgrade target load """ self.config_uuid = config_uuid self.context['config_upgrade'] = config = {} for puppet_plugin in self.puppet_plugins: config.update(puppet_plugin.obj.get_host_config_upgrade(host)) self._merge_host_config(host, target_load, config) LOG.info("Updating hiera for host: %s with config_uuid: %s " "target_load: %s config: %s" % (host.hostname, config_uuid, target_load, config)) def _get_address_by_name(self, name, networktype): """ Retrieve an address entry by name and scoped by network type """ address = utils.get_primary_address_by_name(self.dbapi, utils.format_address_name(name, networktype), networktype, True) return address def _merge_host_config(self, host, target_load, config): filename = host.hostname + '.yaml' path = os.path.join( tsconfig.PLATFORM_PATH, 'puppet', target_load, 'hieradata') # for downgrade ( upgrade-abort ) to STX.8 the hieradata is # still using .yaml hiera_file = os.path.join(path, filename) if self._is_controller0_downgrade(host, hiera_file): mgmt_address = self._get_address_by_name( constants.CONTROLLER_0_HOSTNAME, constants.NETWORK_TYPE_MGMT) filename = mgmt_address + ".yaml" with io.open(os.path.join(path, filename), 'r', encoding='utf-8') as yaml_file: host_config = yaml.load(yaml_file, Loader=yaml.FullLoader) host_config.update(config) self._write_host_config(host, host_config, path, filename) def remove_host_config(self, host): """Remove the configuration for the supplied host""" try: filename = "%s.yaml" % host.hostname self._remove_config(filename) except Exception: LOG.exception("failed to remove host config: %s" % host.uuid) def _write_host_config(self, host, config, path=None, filename=None): """Update the configuration for a specific host""" if filename is None: filename = "%s.yaml" % host.hostname self._write_config(filename, config, path) def _read_host_config(self, host, path): filename = "%s.yaml" % host.hostname return self._read_config(filename, path) def _read_config(self, filename, path): filepath = os.path.join(path, filename) try: LOG.debug("Reading config at %s", filepath) with open(filepath, 'r') as f: return yaml.load(f, Loader=yaml.Loader) except Exception: LOG.exception("Failed to read config file at %s" % filepath) raise def _write_config(self, filename, config, path=None): if path is None: path = self.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(config, f, default_flow_style=False) os.close(fd) os.rename(tmppath, filepath) except Exception: LOG.exception("failed to write config file: %s" % filepath) raise def _remove_config(self, filename): filepath = os.path.join(self.path, filename) try: if os.path.exists(filepath): os.unlink(filepath) except Exception: LOG.exception("failed to delete config file: %s" % filepath) raise