Merge "Decouple bootstrap from cli module"

This commit is contained in:
Zuul 2018-06-05 02:07:13 +00:00 committed by Gerrit Code Review
commit 521552a354
3 changed files with 348 additions and 279 deletions

290
keystone/cmd/bootstrap.py Normal file
View File

@ -0,0 +1,290 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import uuid
from oslo_log import log
from keystone.common import driver_hints
from keystone.common import provider_api
import keystone.conf
from keystone import exception
from keystone.server import backends
CONF = keystone.conf.CONF
LOG = log.getLogger(__name__)
PROVIDERS = provider_api.ProviderAPIs
class Bootstrapper(object):
def __init__(self):
backends.load_backends()
self.admin_password = None
self.admin_username = None
self.project_id = None
self.project_name = None
self.admin_role_id = None
self.admin_role_name = None
self.region_id = None
self.service_name = None
self.public_url = None
self.internal_url = None
self.admin_url = None
self.endpoints = {}
self.default_domain_id = None
self.admin_user_id = None
def bootstrap(self):
# NOTE(morganfainberg): Ensure the default domain is in-fact created
self._bootstrap_default_domain()
self._bootstrap_project()
self._bootstrap_admin_user()
self._bootstrap_admin_role()
self._bootstrap_project_role_assignment()
self._bootstrap_system_role_assignment()
self._bootstrap_region()
self._bootstrap_catalog()
def _bootstrap_default_domain(self):
default_domain = {
'id': CONF.identity.default_domain_id,
'name': 'Default',
'enabled': True,
'description': 'The default domain'
}
try:
PROVIDERS.resource_api.create_domain(
domain_id=default_domain['id'],
domain=default_domain)
LOG.info('Created domain %s', default_domain['id'])
except exception.Conflict:
# NOTE(morganfainberg): Domain already exists, continue on.
LOG.info('Domain %s already exists, skipping creation.',
default_domain['id'])
self.default_domain_id = default_domain['id']
def _bootstrap_project(self):
try:
project_id = uuid.uuid4().hex
project = {
'enabled': True,
'id': project_id,
'domain_id': self.default_domain_id,
'description': 'Bootstrap project for initializing the cloud.',
'name': self.project_name
}
PROVIDERS.resource_api.create_project(project_id, project)
LOG.info('Created project %s', self.project_name)
except exception.Conflict:
LOG.info('Project %s already exists, skipping creation.',
self.project_name)
project = PROVIDERS.resource_api.get_project_by_name(
self.project_name, self.default_domain_id
)
self.project_id = project['id']
def _bootstrap_admin_user(self):
# NOTE(morganfainberg): Do not create the user if it already exists.
try:
user = PROVIDERS.identity_api.get_user_by_name(
self.admin_username, self.default_domain_id
)
LOG.info('User %s already exists, skipping creation.',
self.admin_username)
# If the user is not enabled, re-enable them. This also helps
# provide some useful logging output later.
update = {}
enabled = user['enabled']
if not enabled:
update['enabled'] = True
try:
PROVIDERS.identity_api.driver.authenticate(
user['id'], self.admin_password
)
except AssertionError:
# This means that authentication failed and that we need to
# update the user's password. This is going to persist a
# revocation event that will make all previous tokens for the
# user invalid, which is OK because it falls within the scope
# of revocation. If a password changes, we shouldn't be able to
# use tokens obtained with an old password.
update['password'] = self.admin_password
# Only make a call to update the user if the password has changed
# or the user was previously disabled. This allows bootstrap to act
# as a recovery tool, without having to create a new user.
if update:
user = PROVIDERS.identity_api.update_user(
user['id'], update
)
LOG.info('Reset password for user %s.', self.admin_username)
if not enabled and user['enabled']:
# Although we always try to enable the user, this log
# message only makes sense if we know that the user was
# previously disabled.
LOG.info('Enabled user %s.', self.admin_username)
except exception.UserNotFound:
user = PROVIDERS.identity_api.create_user(
user_ref={
'name': self.admin_username,
'enabled': True,
'domain_id': self.default_domain_id,
'password': self.admin_password
}
)
LOG.info('Created user %s', self.admin_username)
self.admin_user_id = user['id']
def _bootstrap_admin_role(self):
# NOTE(morganfainberg): Do not create the role if it already exists.
try:
role_id = uuid.uuid4().hex
role = {'name': self.admin_role_name, 'id': role_id}
role = PROVIDERS.role_api.create_role(role_id, role)
LOG.info('Created role %s', self.admin_role_name)
except exception.Conflict:
LOG.info(
'Role %s exists, skipping creation.', self.admin_role_name
)
# NOTE(davechen): There is no backend method to get the role
# by name, so build the hints to list the roles and filter by
# name instead.
hints = driver_hints.Hints()
hints.add_filter('name', self.admin_role_name)
role = PROVIDERS.role_api.list_roles(hints)[0]
self.admin_role_id = role['id']
def _bootstrap_project_role_assignment(self):
try:
PROVIDERS.assignment_api.add_role_to_user_and_project(
user_id=self.admin_user_id,
tenant_id=self.project_id,
role_id=self.admin_role_id
)
LOG.info('Granted %(role)s on %(project)s to user'
' %(username)s.',
{'role': self.admin_role_name,
'project': self.project_name,
'username': self.admin_username})
except exception.Conflict:
LOG.info('User %(username)s already has %(role)s on '
'%(project)s.',
{'username': self.admin_username,
'role': self.admin_role_name,
'project': self.project_name})
def _bootstrap_system_role_assignment(self):
# NOTE(lbragstad): We need to make sure a user has at least one role on
# the system. Otherwise it's possible for administrators to lock
# themselves out of system-level APIs in their deployment. This is
# considered backwards compatible because even if the assignment
# exists, it needs to be enabled through oslo.policy configuration
# options to be enforced.
try:
PROVIDERS.assignment_api.create_system_grant_for_user(
self.admin_user_id, self.admin_role_id
)
LOG.info('Granted %(role)s on the system to user'
' %(username)s.',
{'role': self.admin_role_name,
'username': self.admin_username})
except exception.Conflict:
LOG.info('User %(username)s already has %(role)s on '
'the system.',
{'username': self.admin_username,
'role': self.admin_role_name})
def _bootstrap_region(self):
if self.region_id:
try:
PROVIDERS.catalog_api.create_region(
region_ref={'id': self.region_id}
)
LOG.info('Created region %s', self.region_id)
except exception.Conflict:
LOG.info('Region %s exists, skipping creation.',
self.region_id)
def _bootstrap_catalog(self):
if self.public_url or self.admin_url or self.internal_url:
hints = driver_hints.Hints()
hints.add_filter('type', 'identity')
services = PROVIDERS.catalog_api.list_services(hints)
if services:
service = services[0]
hints = driver_hints.Hints()
hints.add_filter('service_id', service['id'])
if self.region_id:
hints.add_filter('region_id', self.region_id)
endpoints = PROVIDERS.catalog_api.list_endpoints(hints)
else:
service_id = uuid.uuid4().hex
service = {
'id': service_id, 'name': self.service_name,
'type': 'identity', 'enabled': True
}
PROVIDERS.catalog_api.create_service(service_id, service)
endpoints = []
self.service_id = service['id']
available_interfaces = {e['interface']: e for e in endpoints}
expected_endpoints = {'public': self.public_url,
'internal': self.internal_url,
'admin': self.admin_url}
for interface, url in expected_endpoints.items():
if not url:
# not specified to bootstrap command
continue
try:
endpoint_ref = available_interfaces[interface]
except KeyError:
endpoint_ref = {'id': uuid.uuid4().hex,
'interface': interface,
'url': url,
'service_id': self.service_id,
'enabled': True}
if self.region_id:
endpoint_ref['region_id'] = self.region_id
PROVIDERS.catalog_api.create_endpoint(
endpoint_id=endpoint_ref['id'],
endpoint_ref=endpoint_ref)
LOG.info('Created %(interface)s endpoint %(url)s',
{'interface': interface, 'url': url})
else:
# NOTE(jamielennox): electing not to update existing
# endpoints here. There may be call to do so in future.
LOG.info('Skipping %s endpoint as already created',
interface)
self.endpoints[interface] = endpoint_ref['id']

View File

@ -26,6 +26,7 @@ from oslo_log import log
from oslo_serialization import jsonutils
import pbr.version
from keystone.cmd import bootstrap
from keystone.cmd import doctor
from keystone.common import driver_hints
from keystone.common import sql
@ -62,20 +63,7 @@ class BootStrap(BaseApp):
name = "bootstrap"
def __init__(self):
self.load_backends()
self.project_id = uuid.uuid4().hex
self.role_id = uuid.uuid4().hex
self.service_id = None
self.service_name = None
self.username = None
self.project_name = None
self.role_name = None
self.password = None
self.public_url = None
self.internal_url = None
self.admin_url = None
self.region_id = None
self.endpoints = {}
self.bootstrapper = bootstrap.Bootstrapper()
@classmethod
def add_argument_parser(cls, subparsers):
@ -124,15 +112,14 @@ class BootStrap(BaseApp):
'process.'))
return parser
def load_backends(self):
drivers = backends.load_backends()
self.resource_manager = drivers['resource_api']
self.identity_manager = drivers['identity_api']
self.assignment_manager = drivers['assignment_api']
self.catalog_manager = drivers['catalog_api']
self.role_manager = drivers['role_api']
def do_bootstrap(self):
"""Perform the bootstrap actions.
def _get_config(self):
Create bootstrap user, project, and role so that CMS, humans, or
scripts can continue to perform initial setup (domains, projects,
services, endpoints, etc) of Keystone when standing up a new
deployment.
"""
self.username = (
os.environ.get('OS_BOOTSTRAP_USERNAME') or
CONF.command.bootstrap_username)
@ -160,236 +147,28 @@ class BootStrap(BaseApp):
self.region_id = (
os.environ.get('OS_BOOTSTRAP_REGION_ID') or
CONF.command.bootstrap_region_id)
def do_bootstrap(self):
"""Perform the bootstrap actions.
Create bootstrap user, project, and role so that CMS, humans, or
scripts can continue to perform initial setup (domains, projects,
services, endpoints, etc) of Keystone when standing up a new
deployment.
"""
self._get_config()
self.service_id = None
self.endpoints = None
if self.password is None:
print(_('Either --bootstrap-password argument or '
'OS_BOOTSTRAP_PASSWORD must be set.'))
raise ValueError
# NOTE(morganfainberg): Ensure the default domain is in-fact created
default_domain = {
'id': CONF.identity.default_domain_id,
'name': 'Default',
'enabled': True,
'description': 'The default domain'
}
try:
self.resource_manager.create_domain(
domain_id=default_domain['id'],
domain=default_domain)
LOG.info('Created domain %s', default_domain['id'])
except exception.Conflict:
# NOTE(morganfainberg): Domain already exists, continue on.
LOG.info('Domain %s already exists, skipping creation.',
default_domain['id'])
self.bootstrapper.admin_password = self.password
self.bootstrapper.admin_username = self.username
self.bootstrapper.project_name = self.project_name
self.bootstrapper.admin_role_name = self.role_name
self.bootstrapper.service_name = self.service_name
self.bootstrapper.service_id = self.service_id
self.bootstrapper.admin_url = self.admin_url
self.bootstrapper.public_url = self.public_url
self.bootstrapper.internal_url = self.internal_url
self.bootstrapper.region_id = self.region_id
try:
self.resource_manager.create_project(
project_id=self.project_id,
project={'enabled': True,
'id': self.project_id,
'domain_id': default_domain['id'],
'description': 'Bootstrap project for initializing '
'the cloud.',
'name': self.project_name}
)
LOG.info('Created project %s', self.project_name)
except exception.Conflict:
LOG.info('Project %s already exists, skipping creation.',
self.project_name)
project = self.resource_manager.get_project_by_name(
self.project_name, default_domain['id'])
self.project_id = project['id']
# NOTE(morganfainberg): Do not create the user if it already exists.
try:
user = self.identity_manager.get_user_by_name(self.username,
default_domain['id'])
LOG.info('User %s already exists, skipping creation.',
self.username)
# If the user is not enabled, re-enable them. This also helps
# provide some useful logging output later.
update = {}
enabled = user['enabled']
if not enabled:
update['enabled'] = True
try:
self.identity_manager.driver.authenticate(
user['id'], self.password
)
except AssertionError:
# This means that authentication failed and that we need to
# update the user's password. This is going to persist a
# revocation event that will make all previous tokens for the
# user invalid, which is OK because it falls within the scope
# of revocation. If a password changes, we shouldn't be able to
# use tokens obtained with an old password.
update['password'] = self.password
# Only make a call to update the user if the password has changed
# or the user was previously disabled. This allows bootstrap to act
# as a recovery tool, without having to create a new user.
if update:
user = self.identity_manager.update_user(user['id'], update)
LOG.info('Reset password for user %s.', self.username)
if not enabled and user['enabled']:
# Although we always try to enable the user, this log
# message only makes sense if we know that the user was
# previously disabled.
LOG.info('Enabled user %s.', self.username)
except exception.UserNotFound:
user = self.identity_manager.create_user(
user_ref={'name': self.username,
'enabled': True,
'domain_id': default_domain['id'],
'password': self.password
}
)
LOG.info('Created user %s', self.username)
# NOTE(morganfainberg): Do not create the role if it already exists.
try:
self.role_manager.create_role(
role_id=self.role_id,
role={'name': self.role_name,
'id': self.role_id},
)
LOG.info('Created role %s', self.role_name)
except exception.Conflict:
LOG.info('Role %s exists, skipping creation.', self.role_name)
# NOTE(davechen): There is no backend method to get the role
# by name, so build the hints to list the roles and filter by
# name instead.
hints = driver_hints.Hints()
hints.add_filter('name', self.role_name)
role = self.role_manager.list_roles(hints)
self.role_id = role[0]['id']
# NOTE(morganfainberg): Handle the case that the role assignment has
# already occurred.
try:
self.assignment_manager.add_role_to_user_and_project(
user_id=user['id'],
tenant_id=self.project_id,
role_id=self.role_id
)
LOG.info('Granted %(role)s on %(project)s to user'
' %(username)s.',
{'role': self.role_name,
'project': self.project_name,
'username': self.username})
except exception.Conflict:
LOG.info('User %(username)s already has %(role)s on '
'%(project)s.',
{'username': self.username,
'role': self.role_name,
'project': self.project_name})
# NOTE(lbragstad): We need to make sure a user has at least one role on
# the system. Otherwise it's possible for administrators to lock
# themselves out of system-level APIs in their deployment. This is
# considered backwards compatible because even if the assignment
# exists, it needs to be enabled through oslo.policy configuration
# options to be enforced.
try:
self.assignment_manager.create_system_grant_for_user(
user['id'], self.role_id
)
LOG.info('Granted %(role)s on the system to user'
' %(username)s.',
{'role': self.role_name,
'username': self.username})
except exception.Conflict:
LOG.info('User %(username)s already has %(role)s on '
'the system.',
{'username': self.username,
'role': self.role_name})
if self.region_id:
try:
self.catalog_manager.create_region(
region_ref={'id': self.region_id}
)
LOG.info('Created region %s', self.region_id)
except exception.Conflict:
LOG.info('Region %s exists, skipping creation.',
self.region_id)
if self.public_url or self.admin_url or self.internal_url:
hints = driver_hints.Hints()
hints.add_filter('type', 'identity')
services = self.catalog_manager.list_services(hints)
if services:
service_ref = services[0]
hints = driver_hints.Hints()
hints.add_filter('service_id', service_ref['id'])
if self.region_id:
hints.add_filter('region_id', self.region_id)
endpoints = self.catalog_manager.list_endpoints(hints)
else:
service_ref = {'id': uuid.uuid4().hex,
'name': self.service_name,
'type': 'identity',
'enabled': True}
self.catalog_manager.create_service(
service_id=service_ref['id'],
service_ref=service_ref)
endpoints = []
self.service_id = service_ref['id']
available_interfaces = {e['interface']: e for e in endpoints}
expected_endpoints = {'public': self.public_url,
'internal': self.internal_url,
'admin': self.admin_url}
for interface, url in expected_endpoints.items():
if not url:
# not specified to bootstrap command
continue
try:
endpoint_ref = available_interfaces[interface]
except KeyError:
endpoint_ref = {'id': uuid.uuid4().hex,
'interface': interface,
'url': url,
'service_id': self.service_id,
'enabled': True}
if self.region_id:
endpoint_ref['region_id'] = self.region_id
self.catalog_manager.create_endpoint(
endpoint_id=endpoint_ref['id'],
endpoint_ref=endpoint_ref)
LOG.info('Created %(interface)s endpoint %(url)s',
{'interface': interface, 'url': url})
else:
# NOTE(jamielennox): electing not to update existing
# endpoints here. There may be call to do so in future.
LOG.info('Skipping %s endpoint as already created',
interface)
self.endpoints[interface] = endpoint_ref['id']
self.bootstrapper.bootstrap()
self.role_id = self.bootstrapper.admin_role_id
self.project_id = self.bootstrapper.project_id
@classmethod
def main(cls):

View File

@ -101,21 +101,21 @@ class CliBootStrapTestCase(unit.SQLDriverOverrides, unit.TestCase):
def _do_test_bootstrap(self, bootstrap):
bootstrap.do_bootstrap()
project = bootstrap.resource_manager.get_project_by_name(
project = PROVIDERS.resource_api.get_project_by_name(
bootstrap.project_name,
'default')
user = bootstrap.identity_manager.get_user_by_name(
user = PROVIDERS.identity_api.get_user_by_name(
bootstrap.username,
'default')
role = bootstrap.role_manager.get_role(bootstrap.role_id)
role = PROVIDERS.role_api.get_role(bootstrap.role_id)
role_list = (
bootstrap.assignment_manager.get_roles_for_user_and_project(
PROVIDERS.assignment_api.get_roles_for_user_and_project(
user['id'],
project['id']))
self.assertIs(1, len(role_list))
self.assertEqual(role_list[0], role['id'])
system_roles = (
bootstrap.assignment_manager.list_system_grants_for_user(
PROVIDERS.assignment_api.list_system_grants_for_user(
user['id']
)
)
@ -123,17 +123,17 @@ class CliBootStrapTestCase(unit.SQLDriverOverrides, unit.TestCase):
self.assertEqual(system_roles[0]['id'], role['id'])
# NOTE(morganfainberg): Pass an empty context, it isn't used by
# `authenticate` method.
bootstrap.identity_manager.authenticate(
PROVIDERS.identity_api.authenticate(
self.make_request(),
user['id'],
bootstrap.password)
if bootstrap.region_id:
region = bootstrap.catalog_manager.get_region(bootstrap.region_id)
region = PROVIDERS.catalog_api.get_region(bootstrap.region_id)
self.assertEqual(self.region_id, region['id'])
if bootstrap.service_id:
svc = bootstrap.catalog_manager.get_service(bootstrap.service_id)
svc = PROVIDERS.catalog_api.get_service(bootstrap.service_id)
self.assertEqual(self.service_name, svc['name'])
self.assertEqual(set(['admin', 'public', 'internal']),
@ -145,7 +145,7 @@ class CliBootStrapTestCase(unit.SQLDriverOverrides, unit.TestCase):
for interface, url in urls.items():
endpoint_id = bootstrap.endpoints[interface]
endpoint = bootstrap.catalog_manager.get_endpoint(endpoint_id)
endpoint = PROVIDERS.catalog_api.get_endpoint(endpoint_id)
self.assertEqual(self.region_id, endpoint['region_id'])
self.assertEqual(url, endpoint['url'])
@ -226,10 +226,10 @@ class CliBootStrapTestCase(unit.SQLDriverOverrides, unit.TestCase):
self._do_test_bootstrap(bootstrap)
# Completely lock the user out.
user_id = bootstrap.identity_manager.get_user_by_name(
user_id = PROVIDERS.identity_api.get_user_by_name(
bootstrap.username,
'default')['id']
bootstrap.identity_manager.update_user(
PROVIDERS.identity_api.update_user(
user_id,
{'enabled': False,
'password': uuid.uuid4().hex})
@ -238,7 +238,7 @@ class CliBootStrapTestCase(unit.SQLDriverOverrides, unit.TestCase):
self._do_test_bootstrap(bootstrap)
# Sanity check that the original password works again.
bootstrap.identity_manager.authenticate(
PROVIDERS.identity_api.authenticate(
self.make_request(),
user_id,
bootstrap.password)
@ -296,68 +296,68 @@ class CliBootStrapTestCaseWithEnvironment(CliBootStrapTestCase):
def test_assignment_created_with_user_exists(self):
# test assignment can be created if user already exists.
bootstrap = cli.BootStrap()
bootstrap.resource_manager.create_domain(self.default_domain['id'],
self.default_domain)
PROVIDERS.resource_api.create_domain(self.default_domain['id'],
self.default_domain)
user_ref = unit.new_user_ref(self.default_domain['id'],
name=self.username,
password=self.password)
bootstrap.identity_manager.create_user(user_ref)
PROVIDERS.identity_api.create_user(user_ref)
self._do_test_bootstrap(bootstrap)
def test_assignment_created_with_project_exists(self):
# test assignment can be created if project already exists.
bootstrap = cli.BootStrap()
bootstrap.resource_manager.create_domain(self.default_domain['id'],
self.default_domain)
PROVIDERS.resource_api.create_domain(self.default_domain['id'],
self.default_domain)
project_ref = unit.new_project_ref(self.default_domain['id'],
name=self.project_name)
bootstrap.resource_manager.create_project(project_ref['id'],
project_ref)
PROVIDERS.resource_api.create_project(project_ref['id'], project_ref)
self._do_test_bootstrap(bootstrap)
def test_assignment_created_with_role_exists(self):
# test assignment can be created if role already exists.
bootstrap = cli.BootStrap()
bootstrap.resource_manager.create_domain(self.default_domain['id'],
self.default_domain)
PROVIDERS.resource_api.create_domain(self.default_domain['id'],
self.default_domain)
role = unit.new_role_ref(name=self.role_name)
bootstrap.role_manager.create_role(role['id'], role)
PROVIDERS.role_api.create_role(role['id'], role)
self._do_test_bootstrap(bootstrap)
def test_assignment_created_with_region_exists(self):
# test assignment can be created if region already exists.
bootstrap = cli.BootStrap()
bootstrap.resource_manager.create_domain(self.default_domain['id'],
self.default_domain)
PROVIDERS.resource_api.create_domain(self.default_domain['id'],
self.default_domain)
region = unit.new_region_ref(id=self.region_id)
bootstrap.catalog_manager.create_region(region)
PROVIDERS.catalog_api.create_region(region)
self._do_test_bootstrap(bootstrap)
def test_endpoints_created_with_service_exists(self):
# test assignment can be created if service already exists.
bootstrap = cli.BootStrap()
bootstrap.resource_manager.create_domain(self.default_domain['id'],
self.default_domain)
PROVIDERS.resource_api.create_domain(self.default_domain['id'],
self.default_domain)
service = unit.new_service_ref(name=self.service_name)
bootstrap.catalog_manager.create_service(service['id'], service)
PROVIDERS.catalog_api.create_service(service['id'], service)
self._do_test_bootstrap(bootstrap)
def test_endpoints_created_with_endpoint_exists(self):
# test assignment can be created if endpoint already exists.
bootstrap = cli.BootStrap()
bootstrap.resource_manager.create_domain(self.default_domain['id'],
self.default_domain)
# test assignment can be created if endpoint already exists.
PROVIDERS.resource_api.create_domain(self.default_domain['id'],
self.default_domain)
service = unit.new_service_ref(name=self.service_name)
bootstrap.catalog_manager.create_service(service['id'], service)
PROVIDERS.catalog_api.create_service(service['id'], service)
region = unit.new_region_ref(id=self.region_id)
bootstrap.catalog_manager.create_region(region)
PROVIDERS.catalog_api.create_region(region)
endpoint = unit.new_endpoint_ref(interface='public',
service_id=service['id'],
url=self.public_url,
region_id=self.region_id)
bootstrap.catalog_manager.create_endpoint(endpoint['id'], endpoint)
PROVIDERS.catalog_api.create_endpoint(endpoint['id'], endpoint)
self._do_test_bootstrap(bootstrap)