# # Copyright (c) 2018-2019 Wind River Systems, Inc. # # SPDX-License-Identifier: Apache-2.0 # import keyring import subprocess from Crypto.PublicKey import RSA from sysinv.helm import base from sysinv.helm import common from oslo_log import log from oslo_serialization import jsonutils from sysinv.common import constants from sysinv.common import exception from sqlalchemy.orm.exc import NoResultFound LOG = log.getLogger(__name__) class OpenstackBaseHelm(base.BaseHelm): """Class to encapsulate Openstack service operations for helm""" SUPPORTED_NAMESPACES = \ base.BaseHelm.SUPPORTED_NAMESPACES + [common.HELM_NS_OPENSTACK] def _get_service_config(self, service): configs = self.context.setdefault('_service_configs', {}) if service not in configs: configs[service] = self._get_service(service) return configs[service] def _get_service_parameters(self, service=None): service_parameters = [] if self.dbapi is None: return service_parameters try: service_parameters = self.dbapi.service_parameter_get_all( service=service) # the service parameter has not been added except NoResultFound: pass return service_parameters def _get_service_parameter_configs(self, service): configs = self.context.setdefault('_service_params', {}) if service not in configs: params = self._get_service_parameters(service) if params: configs[service] = params else: return None return configs[service] @staticmethod def _service_parameter_lookup_one(service_parameters, section, name, default): for param in service_parameters: if param['section'] == section and param['name'] == name: return param['value'] return default def _get_admin_user_name(self): keystone_operator = self._operator.chart_operators[ constants.HELM_CHART_KEYSTONE] return keystone_operator.get_admin_user_name() def _get_identity_password(self, service, user): passwords = self.context.setdefault('_service_passwords', {}) if service not in passwords: passwords[service] = {} if user not in passwords[service]: passwords[service][user] = self._get_keyring_password(service, user) return passwords[service][user] def _get_database_username(self, service): return 'admin-%s' % service def _get_keyring_password(self, service, user, pw_format=None): password = keyring.get_password(service, user) if not password: if pw_format == common.PASSWORD_FORMAT_CEPH: try: cmd = ['ceph-authtool', '--gen-print-key'] password = subprocess.check_output(cmd).strip() except subprocess.CalledProcessError: raise exception.SysinvException( 'Failed to generate ceph key') else: password = self._generate_random_password() keyring.set_password(service, user, password) # get_password() returns in unicode format, which leads to YAML # that Armada doesn't like. Converting to UTF-8 is safe because # we generated the password originally. return password.encode('utf8', 'strict') def _get_service_region_name(self, service): if self._region_config(): service_config = self._get_service_config(service) if (service_config is not None and service_config.region_name is not None): return service_config.region_name if (self._distributed_cloud_role() == constants.DISTRIBUTED_CLOUD_ROLE_SYSTEMCONTROLLER and service in self.SYSTEM_CONTROLLER_SERVICES): return constants.SYSTEM_CONTROLLER_REGION return self._region_name() def _get_configured_service_name(self, service, version=None): if self._region_config(): service_config = self._get_service_config(service) if service_config is not None: name = 'service_name' if version is not None: name = version + '_' + name service_name = service_config.capabilities.get(name) if service_name is not None: return service_name elif version is not None: return service + version else: return service def _get_configured_service_type(self, service, version=None): if self._region_config(): service_config = self._get_service_config(service) if service_config is not None: stype = 'service_type' if version is not None: stype = version + '_' + stype return service_config.capabilities.get(stype) return None def _get_or_generate_password(self, chart, namespace, field): # Get password from the db for the specified chart overrides if not self.dbapi: return None try: override = self.dbapi.helm_override_get(name=chart, namespace=namespace) except exception.HelmOverrideNotFound: # Override for this chart not found, so create one try: values = { 'name': chart, 'namespace': namespace, } override = self.dbapi.helm_override_create(values=values) except Exception as e: LOG.exception(e) return None password = override.system_overrides.get(field, None) if password: return password.encode('utf8', 'strict') # The password is not present, so generate one and store it to # the override password = self._generate_random_password() values = {'system_overrides': override.system_overrides} values['system_overrides'].update({ field: password, }) try: self.dbapi.helm_override_update( name=chart, namespace=namespace, values=values) except Exception as e: LOG.exception(e) return password.encode('utf8', 'strict') def _get_endpoints_identity_overrides(self, service_name, users): # Returns overrides for admin and individual users overrides = {} overrides.update(self._get_common_users_overrides(service_name)) for user in users: overrides.update({ user: { 'region_name': self._region_name(), 'password': self._get_or_generate_password( service_name, common.HELM_NS_OPENSTACK, user) } }) return overrides def _get_endpoints_oslo_db_overrides(self, service_name, users): overrides = { 'admin': { 'password': self._get_common_password('admin_db'), } } for user in users: overrides.update({ user: { 'password': self._get_or_generate_password( service_name, common.HELM_NS_OPENSTACK, user + '_db'), } }) return overrides def _get_endpoints_oslo_messaging_overrides(self, service_name, users): overrides = { 'admin': { 'username': 'rabbitmq-admin', 'password': self._get_common_password('rabbitmq-admin') } } for user in users: overrides.update({ user: { 'username': user + '-rabbitmq-user', 'password': self._get_or_generate_password( service_name, common.HELM_NS_OPENSTACK, user + '_rabbit') } }) return overrides def _get_common_password(self, name): # Admin passwords are stored on keystone's helm override entry return self._get_or_generate_password( 'keystone', common.HELM_NS_OPENSTACK, name) def _get_common_users_overrides(self, service): overrides = {} for user in common.USERS: if user == common.USER_ADMIN: o_user = self._get_admin_user_name() o_service = common.SERVICE_ADMIN else: o_user = user o_service = service overrides.update({ user: { 'region_name': self._region_name(), 'username': o_user, 'password': self._get_identity_password(o_service, o_user) } }) return overrides def _get_ceph_password(self, service, user): passwords = self.context.setdefault('_ceph_passwords', {}) if service not in passwords: passwords[service] = {} if user not in passwords[service]: passwords[service][user] = self._get_keyring_password( service, user, pw_format=common.PASSWORD_FORMAT_CEPH) return passwords[service][user] def _get_or_generate_ssh_keys(self, chart, namespace): try: override = self.dbapi.helm_override_get(name=chart, namespace=namespace) except exception.HelmOverrideNotFound: # Override for this chart not found, so create one values = { 'name': chart, 'namespace': namespace, } override = self.dbapi.helm_override_create(values=values) privatekey = override.system_overrides.get('privatekey', None) publickey = override.system_overrides.get('publickey', None) if privatekey and publickey: return str(privatekey), str(publickey) # ssh keys are not set so generate them and store in overrides key = RSA.generate(2048) pubkey = key.publickey() newprivatekey = key.exportKey('PEM') newpublickey = pubkey.exportKey('OpenSSH') values = {'system_overrides': override.system_overrides} values['system_overrides'].update({'privatekey': newprivatekey, 'publickey': newpublickey}) self.dbapi.helm_override_update( name=chart, namespace=namespace, values=values) return newprivatekey, newpublickey def _oslo_multistring_override(self, name=None, values=[]): """ Generate helm multistring dictionary override for specified option name with multiple values. This generates oslo_config.MultiStringOpt() compatible config with multiple input values. This routine JSON encodes each value for complex types (eg, dict, list, set). Return a multistring type formatted dictionary override. """ override = None if name is None or not values: return override mvalues = [] for value in values: if isinstance(value, (dict, list, set)): mvalues.append(jsonutils.dumps(value)) else: mvalues.append(value) override = { name: {'type': 'multistring', 'values': mvalues, } } return override