Skip to content

Commit

Permalink
1132663 - pulp-manage-db now has a --dry-run flag.
Browse files Browse the repository at this point in the history
This flag causes pulp-manage-db to returns 1 if changes
would have been made by pulp-manage-db and 0 if everything is up-to-date.
  • Loading branch information
jeremycline committed Nov 17, 2014
1 parent 67ec80d commit fe3041b
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 23 deletions.
39 changes: 36 additions & 3 deletions server/pulp/plugins/loader/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ def get_cataloger_by_id(catloger_id):
return cls(), cfg


def load_content_types(types_dir=_TYPES_DIR):
def load_content_types(types_dir=_TYPES_DIR, dry_run=False):
"""
:type types_dir: str
"""
Expand All @@ -411,11 +411,14 @@ def load_content_types(types_dir=_TYPES_DIR):
_logger.critical(msg % {'p': types_dir})
raise IOError(msg % {'p': types_dir})
descriptors = _load_type_descriptors(types_dir)
_load_type_definitions(descriptors)

if dry_run:
return _check_content_definitions(descriptors)
else:
_load_type_definitions(descriptors)

# initialization methods -------------------------------------------------------


def _is_initialized():
"""
:rtype: bool
Expand All @@ -428,6 +431,36 @@ def _create_manager():
_MANAGER = PluginManager()


def _check_content_definitions(descriptors):
"""
Check whether the given content definitions exist in the database. This method
does not make any changes to the content definitions or any indexes.
:param descriptors: A list of content descriptors
:type descriptors: list of TypeDescriptor
:return: A list of content types that would have been created or updated by _load_type_definitions
:rtype: list of TypeDefinition
"""
definitions = parser.parse(descriptors)
old_content_types = []

# Ensure all the content types exist and match the definitions
for definition in definitions:
content_type = database.type_definition(definition.id)
if content_type is None:
old_content_types.append(definition)
continue

dict_definition = definition.__dict__
for key, value in dict_definition.items():
if key not in content_type or content_type[key] != value:
old_content_types.append(definition)
break

return old_content_types


def _load_type_descriptors(path):
"""
:type path: str
Expand Down
13 changes: 13 additions & 0 deletions server/pulp/plugins/types/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

# -- database exceptions ------------------------------------------------------


class UpdateFailed(Exception):
"""
Indicates a call to update the database has failed for one or more type
Expand Down Expand Up @@ -64,6 +65,7 @@ def __str__(self):

# -- public -------------------------------------------------------------------


def update_database(definitions, error_on_missing_definitions=False):
"""
Brings the database up to date with the types defined in the given
Expand Down Expand Up @@ -254,8 +256,18 @@ def type_units_unit_key(type_id):

# -- private -----------------------------------------------------------------


def _create_or_update_type(type_def):
"""
This method creates or updates a type definition in MongoDB.
:param type_def: the type definition to update or create. If a type definition with the same
as an existing type, the type is updated, otherwise it is created.
:type type_def: ContentType
:return: This method will always return None
:rtype: None
"""
# Make sure a collection exists for the type
database = pulp_db.get_database()
collection_name = unit_collection_name(type_def.id)
Expand All @@ -274,6 +286,7 @@ def _create_or_update_type(type_def):
# XXX this still causes a potential race condition when 2 users are updating the same type
content_type_collection.save(content_type, safe=True)


def _update_indexes(type_def, unique):

collection_name = unit_collection_name(type_def.id)
Expand Down
68 changes: 55 additions & 13 deletions server/pulp/server/db/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from pulp.server.db import connection
from pulp.server.db.migrate import models
from pulp.server.managers import factory
from pulp.server.managers.auth.role.cud import RoleManager
from pulp.server.managers.auth.role.cud import RoleManager, SUPER_USER_ROLE
from pulp.server.managers.auth.user.cud import UserManager


Expand All @@ -37,6 +37,13 @@ class DataError(Exception):
pass


class UnperformedMigrationException(Exception):
"""
This exception is raised when there are unperformed exceptions.
"""
pass


def parse_args():
"""
Parse the command line arguments into the flags that we accept. Returns the parsed options.
Expand All @@ -45,6 +52,8 @@ def parse_args():
parser.add_option('--test', action='store_true', dest='test',
default=False,
help=_('Run migration, but do not update version'))
parser.add_option('--dry-run', action='store_true', dest='dry_run', default=False,
help=_('Perform a dry run with no changes made. Returns 1 if there are migrations to apply.'))
options, args = parser.parse_args()
if args:
parser.error(_('Unknown arguments: %s') % ', '.join(args))
Expand All @@ -58,6 +67,7 @@ def migrate_database(options):
:param options: The command line parameters from the user
"""
migration_packages = models.get_migration_packages()
unperformed_migrations = False
for migration_package in migration_packages:
if migration_package.current_version > migration_package.latest_available_version:
msg = _('The database for migration package %(p)s is at version %(v)s, which is larger '
Expand All @@ -77,13 +87,18 @@ def migrate_database(options):
message = _('Applying %(p)s version %(v)s')
message = message % {'p': migration_package.name, 'v': migration.version}
logger.info(message)
# We pass in !options.test to stop the apply_migration method from updating the
# package's current version when the --test flag is set
migration_package.apply_migration(migration,
update_current_version=not options.test)
message = _('Migration to %(p)s version %(v)s complete.')
message = message % {'p': migration_package.name,
'v': migration_package.current_version}
if options.dry_run:
unperformed_migrations = True
message = _('Would have applied migration to %(p)s version %(v)s')
message = message % {'p': migration_package.name, 'v': migration.version}
else:
# We pass in !options.test to stop the apply_migration method from updating the
# package's current version when the --test flag is set
migration_package.apply_migration(migration,
update_current_version=not options.test)
message = _('Migration to %(p)s version %(v)s complete.')
message = message % {'p': migration_package.name,
'v': migration_package.current_version}
logger.info(message)
except Exception, e:
# Log the error and what migration failed before allowing main() to handle the exception
Expand All @@ -92,6 +107,9 @@ def migrate_database(options):
logger.critical(error_message)
raise

if options.dry_run and unperformed_migrations:
raise UnperformedMigrationException


def main():
"""
Expand All @@ -104,7 +122,9 @@ def main():
options = parse_args()
_start_logging()
connection.initialize(max_timeout=1)
_auto_manage_db(options)
return _auto_manage_db(options)
except UnperformedMigrationException:
return 1
except DataError, e:
logger.critical(str(e))
logger.critical(''.join(traceback.format_exception(*sys.exc_info())))
Expand All @@ -113,7 +133,6 @@ def main():
logger.critical(str(e))
logger.critical(''.join(traceback.format_exception(*sys.exc_info())))
return os.EX_SOFTWARE
return os.EX_OK


def _auto_manage_db(options):
Expand All @@ -123,9 +142,16 @@ def _auto_manage_db(options):
:param options: The command line parameters from the user.
"""
unperformed_migrations = False

message = _('Loading content types.')
logger.info(message)
load_content_types()
# Note that if dry_run is False, None is always returned
old_content_types = load_content_types(dry_run=options.dry_run)
if old_content_types:
for content_type in old_content_types:
message = _('Would have created or updated the following type definition: ' + content_type.id)
logger.info(message)
message = _('Content types loaded.')
logger.info(message)

Expand All @@ -135,9 +161,22 @@ def _auto_manage_db(options):
# RoleManager are going to try to use it.
factory.initialize()
role_manager = RoleManager()
role_manager.ensure_super_user_role()
if options.dry_run:
if not role_manager.get_role(SUPER_USER_ROLE):
unperformed_migrations = True
message = _('Would have created the admin role.')
logger.info(message)
else:
role_manager.ensure_super_user_role()

user_manager = UserManager()
user_manager.ensure_admin()
if options.dry_run:
if not user_manager.get_admins():
unperformed_migrations = True
message = _('Would have created the default admin user.')
logger.info(message)
else:
user_manager.ensure_admin()
message = _('Admin role and user are in place.')
logger.info(message)

Expand All @@ -147,6 +186,9 @@ def _auto_manage_db(options):
message = _('Database migrations complete.')
logger.info(message)

if unperformed_migrations:
return 1

return os.EX_OK


Expand Down
15 changes: 14 additions & 1 deletion server/pulp/server/managers/auth/role/cud.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,13 +294,26 @@ def ensure_super_user_role(self):
"""
Ensure that the super user role exists.
"""
role = Role.get_collection().find_one({'id': SUPER_USER_ROLE})
role = self.get_role(SUPER_USER_ROLE)
if role is None:
role = self.create_role(SUPER_USER_ROLE, 'Super Users',
'Role indicates users with admin privileges')
role['permissions'] = {'/': [CREATE, READ, UPDATE, DELETE, EXECUTE]}
Role.get_collection().save(role, safe=True)

@staticmethod
def get_role(role):
"""
Get a Role by id.
:param role: A role id to search for
:type role: str
:return: a Role object that have the given role id.
:rtype: Role or None
"""
return Role.get_collection().find_one({'id': role})


add_permissions_to_role = task(RoleManager.add_permissions_to_role, base=Task, ignore_result=True)
add_user_to_role = task(RoleManager.add_user_to_role, base=Task, ignore_result=True)
Expand Down
20 changes: 16 additions & 4 deletions server/pulp/server/managers/auth/user/cud.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,8 @@ def ensure_admin(self):
If no super users are found, the default admin user (from the pulp config)
is looked up or created and added to the super users role.
"""
user_query_manager = factory.user_query_manager()
role_manager = factory.role_manager()

super_users = user_query_manager.find_users_belonging_to_role(SUPER_USER_ROLE)
if super_users:
if self.get_admins():
return

default_login = config.config.get('server', 'default_login')
Expand All @@ -215,6 +212,21 @@ def ensure_admin(self):

role_manager.add_user_to_role(SUPER_USER_ROLE, default_login)

@staticmethod
def get_admins():
"""
Get a list of users with the super-user role.
:return: list of users who are admins.
:rtype: list of User
"""
user_query_manager = factory.user_query_manager()

try:
super_users = user_query_manager.find_users_belonging_to_role(SUPER_USER_ROLE)
return super_users
except MissingResource:
return None

create_user = task(UserManager.create_user, base=Task)
delete_user = task(UserManager.delete_user, base=Task, ignore_result=True)
Expand Down
Empty file.
69 changes: 69 additions & 0 deletions server/test/unit/plugins/loader/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import mock

from ... import base
from pulp.plugins.loader import api
from pulp.plugins.types.model import TypeDefinition


class LoaderApiTests(base.PulpServerTests):
"""
This class tests the pulp.plugins.loader.api module.
"""

@mock.patch('pulp.plugins.loader.api._load_type_definitions', autospec=True)
@mock.patch('pulp.plugins.loader.api._check_content_definitions', autospec=True)
def test_load_content_types_dry_run(self, mock_check_content, mock_load_type):
"""
Test that calling load_content_types with dry_run=True results in checking the content types
rather than loading them.
"""
api.load_content_types(dry_run=True)
self.assertEquals(1, mock_check_content.call_count)
self.assertEquals(0, mock_load_type.call_count)

@mock.patch('pulp.plugins.types.parser.parse', autospec=True)
@mock.patch('pulp.plugins.types.database.type_definition', autospec=True)
def test_check_content_definitions_nothing_old(self, mock_type_definition, mock_parser):
"""
Test that when the content type from the database matches the TypeDefinition,
an empty list is returned.
"""
fake_type = {
'id': 'steve_holt',
'display_name': 'STEVE HOLT!',
'description': 'STEVE HOLT!',
'unit_key': ['STEVE HOLT!'],
'search_indexes': ['STEVE HOLT!'],
'referenced_types': ['STEVE HOLT!'],
}
mock_type_definition.return_value = fake_type
type_definition = TypeDefinition('steve_holt', 'STEVE HOLT!', 'STEVE HOLT!', 'STEVE HOLT!',
'STEVE HOLT!', 'STEVE HOLT!')
mock_parser.return_value = [type_definition]

result = api._check_content_definitions([])
self.assertEquals(0, len(result))

@mock.patch('pulp.plugins.types.parser.parse', autospec=True)
@mock.patch('pulp.plugins.types.database.type_definition', autospec=True)
def test_check_content_definitions_old(self, mock_type_definition, mock_parser):
"""
Test that when the content type from the database doesn't match the TypeDefinition,
the list contains that content type.
"""
fake_type = {
'id': 'gob',
'display_name': 'Trickster',
'description': 'Trickster',
'unit_key': ['Trickster'],
'search_indexes': ['Trickster'],
'referenced_types': ['Trickster'],
}
mock_type_definition.return_value = fake_type
type_definition = TypeDefinition('gob', 'STEVE HOLT!', 'STEVE HOLT!', 'STEVE HOLT!',
'STEVE HOLT!', 'STEVE HOLT!')
mock_parser.return_value = [type_definition]

result = api._check_content_definitions([])
self.assertEquals(1, len(result))
self.assertEquals(result[0], type_definition)
Loading

0 comments on commit fe3041b

Please sign in to comment.