config/sysinv/sysinv/sysinv/sysinv/puppet/keystone.py

396 lines
16 KiB
Python

#
# Copyright (c) 2017 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from six.moves import configparser
import os
from sysinv.common import constants
from tsconfig import tsconfig
from six.moves.urllib.parse import urlparse
from sysinv.puppet import openstack
OPENSTACK_PASSWORD_RULES_FILE = '/etc/keystone/password-rules.conf'
class KeystonePuppet(openstack.OpenstackBasePuppet):
"""Class to encapsulate puppet operations for keystone configuration"""
SERVICE_NAME = 'keystone'
SERVICE_TYPE = 'identity'
SERVICE_PORT = 5000
SERVICE_PATH = 'v3'
ADMIN_SERVICE = 'CGCS'
ADMIN_USER = 'admin'
DEFAULT_DOMAIN_NAME = 'Default'
def _region_config(self):
# A wrapper over the Base region_config check.
if (self._distributed_cloud_role() ==
constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD):
return False
else:
return super(KeystonePuppet, self)._region_config()
def get_static_config(self):
dbuser = self._get_database_username(self.SERVICE_NAME)
admin_username = self.get_admin_user_name()
return {
'keystone::db::postgresql::user': dbuser,
'platform::client::params::admin_username': admin_username,
'platform::client::credentials::params::keyring_base':
os.path.dirname(tsconfig.KEYRING_PATH),
'platform::client::credentials::params::keyring_directory':
tsconfig.KEYRING_PATH,
'platform::client::credentials::params::keyring_file':
os.path.join(tsconfig.KEYRING_PATH, '.CREDENTIAL'),
}
def get_secure_static_config(self):
dbpass = self._get_database_password(self.SERVICE_NAME)
admin_password = self._get_keyring_password(self.ADMIN_SERVICE,
self.ADMIN_USER)
admin_token = self._generate_random_password(length=32)
# initial bootstrap is bound to localhost
dburl = self._format_database_connection(self.SERVICE_NAME,
constants.LOCALHOST_HOSTNAME)
return {
'keystone::database_connection': dburl,
'keystone::admin_password': admin_password,
'keystone::admin_token': admin_token,
'keystone::db::postgresql::password': dbpass,
'keystone::roles::admin::password': admin_password,
}
def get_system_config(self):
admin_username = self.get_admin_user_name()
admin_project = self.get_admin_project_name()
config = {
'keystone::public_bind_host': self._get_management_address(),
'keystone::admin_bind_host': self._get_management_address(),
'keystone::endpoint::public_url': self.get_public_url(),
'keystone::endpoint::internal_url': self.get_internal_url(),
'keystone::endpoint::admin_url': self.get_admin_url(),
'keystone::endpoint::region': self._region_name(),
'keystone::roles::admin::admin': admin_username,
'platform::client::params::admin_username': admin_username,
'platform::client::params::admin_project_name': admin_project,
'platform::client::params::admin_user_domain':
self.get_admin_user_domain(),
'platform::client::params::admin_project_domain':
self.get_admin_project_domain(),
'platform::client::params::identity_region': self._region_name(),
'platform::client::params::identity_auth_url': self.get_auth_url(),
'platform::client::params::keystone_identity_region':
self._identity_specific_region_name(),
'platform::client::params::auth_region':
self._identity_specific_region_name(),
'openstack::keystone::params::api_version': self.SERVICE_PATH,
'openstack::keystone::params::identity_uri':
self.get_identity_uri(),
'openstack::keystone::params::auth_uri':
self.get_auth_uri(),
'openstack::keystone::params::host_url':
self._format_url_address(self._get_management_address()),
# The region in which the identity server can be found
# and it could be different than the region where the
# system resides
'openstack::keystone::params::region_name':
self._identity_specific_region_name(),
'openstack::keystone::params::system_controller_region':
constants.SYSTEM_CONTROLLER_REGION,
'openstack::keystone::params::service_create':
self._to_create_services(),
'CONFIG_KEYSTONE_ADMIN_USERNAME': self.get_admin_user_name(),
}
config.update(self._get_service_parameter_config())
config.update(self._get_password_rule())
return config
def get_secure_system_config(self):
# the admin password may have been updated since initial
# configuration. Retrieve the password from keyring and
# update the hiera records
admin_password = self._get_keyring_password(self.ADMIN_SERVICE,
self.ADMIN_USER)
db_connection = self._format_database_connection(self.SERVICE_NAME)
config = {
'keystone::admin_password': admin_password,
'keystone::roles::admin::password': admin_password,
'keystone::database_connection': db_connection,
}
return config
def _get_service_parameter_config(self):
service_parameters = self._get_service_parameter_configs(
constants.SERVICE_TYPE_IDENTITY)
if service_parameters is None:
return {}
identity_backend = self._service_parameter_lookup_one(
service_parameters,
constants.SERVICE_PARAM_SECTION_IDENTITY_IDENTITY,
constants.SERVICE_PARAM_IDENTITY_DRIVER,
constants.SERVICE_PARAM_IDENTITY_IDENTITY_DRIVER_SQL)
config = {
'keystone::ldap::identity_driver': identity_backend,
'openstack::keystone::params::token_expiration':
self._service_parameter_lookup_one(
service_parameters,
constants.SERVICE_PARAM_SECTION_IDENTITY_CONFIG,
constants.SERVICE_PARAM_IDENTITY_CONFIG_TOKEN_EXPIRATION,
constants.SERVICE_PARAM_IDENTITY_CONFIG_TOKEN_EXPIRATION_DEFAULT),
}
if identity_backend == constants.SERVICE_PARAM_IDENTITY_IDENTITY_DRIVER_LDAP:
# If Keystone's Identity backend has been specified as
# LDAP, then redirect that to Titanium's Hybrid driver
# which is an abstraction over both the SQL and LDAP backends,
# since we still need to support SQL backend operations, without
# necessarily moving it into a separate domain
config['keystone::ldap::identity_driver'] = 'hybrid'
basic_options = ['url', 'suffix', 'user', 'password',
'user_tree_dn', 'user_objectclass',
'query_scope',
'page_size', 'debug_level']
use_tls = self._service_parameter_lookup_one(
service_parameters,
constants.SERVICE_PARAM_SECTION_IDENTITY_LDAP,
'use_tls', False)
if use_tls:
tls_options = ['use_tls', 'tls_cacertdir', 'tls_cacertfile',
'tls_req_cert']
basic_options.extend(tls_options)
user_options = ['user_filter', 'user_id_attribute',
'user_name_attribute', 'user_mail_attribute',
'user_enabled_attribute', 'user_enabled_mask',
'user_enabled_default', 'user_enabled_invert',
'user_attribute_ignore',
'user_default_project_id_attribute',
'user_pass_attribute',
'user_enabled_emulation',
'user_enabled_emulation_dn',
'user_additional_attribute_mapping']
basic_options.extend(user_options)
group_options = ['group_tree_dn', 'group_filter',
'group_objectclass', 'group_id_attribute',
'group_name_attribute', 'group_member_attribute',
'group_desc_attribute', 'group_attribute_ignore',
'group_additional_attribute_mapping']
basic_options.extend(group_options)
use_pool = self._service_parameter_lookup_one(
service_parameters,
constants.SERVICE_PARAM_SECTION_IDENTITY_LDAP,
'use_pool', False)
if use_pool:
pool_options = ['use_pool', 'pool_size', 'pool_retry_max',
'pool_retry_delay', 'pool_connection_timeout',
'pool_connection_lifetime', 'use_auth_pool',
'auth_pool_size',
'auth_pool_connection_lifetime']
basic_options.extend(pool_options)
for opt in basic_options:
config.update(self._format_service_parameter(
service_parameters,
constants.SERVICE_PARAM_SECTION_IDENTITY_LDAP,
'keystone::ldap::', opt))
return config
@staticmethod
def _get_password_rule():
password_rule = {}
if os.path.isfile(OPENSTACK_PASSWORD_RULES_FILE):
try:
passwd_rules = \
KeystonePuppet._extract_openstack_password_rules_from_file(
OPENSTACK_PASSWORD_RULES_FILE)
password_rule.update({
'keystone::security_compliance::unique_last_password_count':
passwd_rules['unique_last_password_count'],
'keystone::security_compliance::password_regex':
passwd_rules['password_regex'],
'keystone::security_compliance::password_regex_description':
passwd_rules['password_regex_description']
})
except Exception:
pass
return password_rule
def _identity_specific_region_name(self):
"""
Returns the Identity Region name based on the System mode:
If Multi-Region then Keystone is shared: return Primary Region
Else: Local Region
"""
if (self._region_config()):
return self.get_region_name()
else:
return self._region_name()
def get_public_url(self):
if (self._region_config() and
self.SERVICE_TYPE in self._get_shared_services()):
return self._get_public_url_from_service_config(self.SERVICE_NAME)
else:
return self._format_public_endpoint(self.SERVICE_PORT)
def get_internal_url(self):
if (self._region_config() and
self.SERVICE_TYPE in self._get_shared_services()):
return self._get_internal_url_from_service_config(self.SERVICE_NAME)
else:
return self._format_private_endpoint(self.SERVICE_PORT)
def get_admin_url(self):
if (self._region_config() and
self.SERVICE_TYPE in self._get_shared_services()):
return self._get_admin_url_from_service_config(self.SERVICE_NAME)
else:
return self._format_private_endpoint(self.SERVICE_PORT)
def get_auth_address(self):
if self._region_config():
url = urlparse(self.get_identity_uri())
return url.hostname
else:
return self._get_management_address()
def get_auth_host(self):
return self._format_url_address(self.get_auth_address())
def get_auth_port(self):
return self.SERVICE_PORT
def get_auth_uri(self):
if self._region_config():
service_config = self._get_service_config(self.SERVICE_NAME)
return service_config.capabilities.get('auth_uri')
else:
return "http://%s:5000" % self._format_url_address(
self._get_management_address())
def get_identity_uri(self):
if self._region_config():
service_config = self._get_service_config(self.SERVICE_NAME)
return service_config.capabilities.get('auth_url')
else:
return "http://%s:%s" % (self._format_url_address(
self._get_management_address()), self.SERVICE_PORT)
def get_auth_url(self):
if self._region_config():
service_config = self._get_service_config(self.SERVICE_NAME)
return service_config.capabilities.get('auth_uri') + '/v3'
else:
return self._format_private_endpoint(self.SERVICE_PORT,
path=self.SERVICE_PATH)
def get_region_name(self):
"""This is a wrapper to get the service region name,
each puppet operator provides this wrap to get the region name
of the service it owns
"""
return self._get_service_region_name(self.SERVICE_NAME)
def get_admin_user_name(self):
if self._region_config():
service_config = self._get_service_config(self.SERVICE_NAME)
if service_config is not None:
return service_config.capabilities.get('admin_user_name')
return self.ADMIN_USER
def get_admin_user_domain(self):
if self._region_config():
service_config = self._get_service_config(self.SERVICE_NAME)
if service_config is not None:
return service_config.capabilities.get('admin_user_domain')
return self.DEFAULT_DOMAIN_NAME
def get_admin_project_name(self):
if self._region_config():
service_config = self._get_service_config(self.SERVICE_NAME)
if service_config is not None:
return service_config.capabilities.get('admin_project_name')
return self.ADMIN_USER
def get_admin_project_domain(self):
if self._region_config():
service_config = self._get_service_config(self.SERVICE_NAME)
if service_config is not None:
return service_config.capabilities.get('admin_project_domain')
return self.DEFAULT_DOMAIN_NAME
def get_service_user_domain(self):
if self._region_config():
service_config = self._get_service_config(self.SERVICE_NAME)
if service_config is not None:
return service_config.capabilities.get('service_user_domain')
return self.DEFAULT_DOMAIN_NAME
def get_service_project_domain(self):
if self._region_config():
service_config = self._get_service_config(self.SERVICE_NAME)
if service_config is not None:
return service_config.capabilities.get('service_project_domain')
return self.DEFAULT_DOMAIN_NAME
def get_service_name(self):
return self._get_configured_service_name(self.SERVICE_NAME)
def get_service_type(self):
service_type = self._get_configured_service_type(self.SERVICE_NAME)
if service_type is None:
return self.SERVICE_TYPE
else:
return service_type
@staticmethod
def _extract_openstack_password_rules_from_file(
rules_file, section="security_compliance"):
try:
config = configparser.RawConfigParser()
parsed_config = config.read(rules_file)
if not parsed_config:
msg = ("Cannot parse rules file: %s" % rules_file)
raise Exception(msg)
if not config.has_section(section):
msg = ("Required section '%s' not found in rules file" % section)
raise Exception(msg)
rules = config.items(section)
if not rules:
msg = ("section '%s' contains no configuration options" % section)
raise Exception(msg)
return dict(rules)
except Exception:
raise Exception("Failed to extract password rules from file")