396 lines
16 KiB
Python
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")
|