diff --git a/pillar/api/utils/__init__.py b/pillar/api/utils/__init__.py index 413413db..075d3f8a 100644 --- a/pillar/api/utils/__init__.py +++ b/pillar/api/utils/__init__.py @@ -158,7 +158,12 @@ class MetaFalsey(type): return False -class DoesNotExist(object, metaclass=MetaFalsey): +class DoesNotExistMeta(MetaFalsey): + def __repr__(cls) -> str: + return 'DoesNotExist' + + +class DoesNotExist(object, metaclass=DoesNotExistMeta): """Returned as value by doc_diff if a value does not exist.""" diff --git a/pillar/cli/maintenance.py b/pillar/cli/maintenance.py index 659dda11..adfb39a1 100644 --- a/pillar/cli/maintenance.py +++ b/pillar/cli/maintenance.py @@ -412,13 +412,17 @@ def expire_all_project_links(project_uuid): @manager_maintenance.option('-m', '--missing', dest='missing', action='store_true', default=False, help='Add missing node types. Note that this may add unwanted ones.') -def replace_pillar_node_type_schemas(proj_url=None, all_projects=False, missing=False): +@manager_maintenance.option('-g', '--go', dest='gogogo', + action='store_true', default=False, + help='Actually go and perform the changes, without this just ' + 'shows differences.') +def replace_pillar_node_type_schemas(project_url=None, all_projects=False, missing=False, go=False): """Replaces the project's node type schemas with the standard Pillar ones. Non-standard node types are left alone. """ - if bool(proj_url) == all_projects: + if bool(project_url) == all_projects: log.error('Use either --project or --all.') return 1 @@ -426,57 +430,73 @@ def replace_pillar_node_type_schemas(proj_url=None, all_projects=False, missing= force_cli_user() from pillar.api.node_types import PILLAR_NAMED_NODE_TYPES - from pillar.api.utils import remove_private_keys + from pillar.api.utils import remove_private_keys, doc_diff projects_collection = current_app.db()['projects'] + will_would = 'Will' if go else 'Would' - def handle_project(project): - log.info('Handling project %s', project['url']) - is_public_proj = not project.get('is_private', True) + def handle_project(proj): + orig_proj = copy.deepcopy(proj) + proj_id = proj['_id'] + if 'url' not in proj: + log.warning('Project %s has no URL!', proj_id) + proj_url = proj.get('url', f'-no URL id {proj_id}') + log.debug('Handling project %s', proj_url) - for proj_nt in project['node_types']: + for proj_nt in proj['node_types']: nt_name = proj_nt['name'] try: pillar_nt = PILLAR_NAMED_NODE_TYPES[nt_name] except KeyError: - log.info(' - skipping non-standard node type "%s"', nt_name) + log.debug(' - skipping non-standard node type "%s"', nt_name) continue - log.info(' - replacing schema on node type "%s"', nt_name) + log.debug(' - replacing schema on node type "%s"', nt_name) # This leaves node type keys intact that aren't in Pillar's node_type_xxx definitions, - # such as permissions. + # such as permissions. It also keeps form schemas as-is. + pillar_nt.pop('form_schema', None) proj_nt.update(copy.deepcopy(pillar_nt)) - # On our own public projects we want to be able to set license stuff. - if is_public_proj: - proj_nt['form_schema'].pop('license_type', None) - proj_nt['form_schema'].pop('license_notes', None) - # Find new node types that aren't in the project yet. if missing: - project_ntnames = set(nt['name'] for nt in project['node_types']) + project_ntnames = set(nt['name'] for nt in proj['node_types']) for nt_name in set(PILLAR_NAMED_NODE_TYPES.keys()) - project_ntnames: log.info(' - Adding node type "%s"', nt_name) pillar_nt = PILLAR_NAMED_NODE_TYPES[nt_name] - project['node_types'].append(copy.deepcopy(pillar_nt)) + proj['node_types'].append(copy.deepcopy(pillar_nt)) - # Use Eve to PUT, so we have schema checking. - db_proj = remove_private_keys(project) - r, _, _, status = current_app.put_internal('projects', db_proj, _id=project['_id']) - if status != 200: - log.error('Error %i storing altered project %s %s', status, project['_id'], r) - raise SystemExit('Error storing project, see log.') - log.info('Project saved succesfully.') + proj_header_shown = False + for key, val1, val2 in doc_diff(orig_proj, proj, falsey_is_equal=False): + if not proj_header_shown: + if proj.get('_deleted', False): + deleted = ' (deleted)' + else: + deleted = '' + log.info('%s change project %s%s', will_would, proj_url, deleted) + proj_header_shown = True + log.info(' %30r: %r → %r', key, val1, val2) + + if go: + # Use Eve to PUT, so we have schema checking. + db_proj = remove_private_keys(proj) + r, _, _, status = current_app.put_internal('projects', db_proj, _id=proj['_id']) + if status != 200: + log.error('Error %i storing altered project %s %s', status, proj['_id'], r) + raise SystemExit('Error storing project, see log.') + log.info('Project saved succesfully.') + + if not go: + log.info('Not changing anything, use --go to actually go and change things.') if all_projects: for project in projects_collection.find(): handle_project(project) return - project = projects_collection.find_one({'url': proj_url}) + project = projects_collection.find_one({'url': project_url}) if not project: - log.error('Project url=%s not found', proj_url) + log.error('Project url=%s not found', project_url) return 3 handle_project(project) diff --git a/tests/test_api/test_cli.py b/tests/test_api/test_cli.py index 741b4fb2..e30951a6 100644 --- a/tests/test_api/test_cli.py +++ b/tests/test_api/test_cli.py @@ -293,7 +293,7 @@ class ReplaceNodeTypesTest(AbstractNodeReplacementTest): # Run the CLI command with self.app.test_request_context(): - replace_pillar_node_type_schemas(proj_url=self.proj['url']) + replace_pillar_node_type_schemas(project_url=self.proj['url']) # Fetch the project again from MongoDB dbproj = self.fetch_project_from_db()