From a9937380b0642cd853537c2e4874939e7e9bf6c3 Mon Sep 17 00:00:00 2001 From: Linus Vogel Date: Thu, 14 May 2026 18:07:26 +0200 Subject: [PATCH] added some logging --- pillar_tool/routers/environment.py | 45 +++++++++++++++++++++++--- pillar_tool/routers/host.py | 23 ++++++++++++++ pillar_tool/routers/hostgroup.py | 28 +++++++++++++++- pillar_tool/routers/pillar.py | 33 +++++++++++++++++++ pillar_tool/routers/state.py | 32 +++++++++++++++++++ pillar_tool/routers/top.py | 51 ++++++++++++++++++++++++++++-- 6 files changed, 204 insertions(+), 8 deletions(-) diff --git a/pillar_tool/routers/environment.py b/pillar_tool/routers/environment.py index 6a83279..c896bdf 100644 --- a/pillar_tool/routers/environment.py +++ b/pillar_tool/routers/environment.py @@ -37,9 +37,12 @@ def environments_get(req: Request): """ db: Session = req.state.db + print("[DEBUG] environments_get: retrieving all environments") result = db.execute(select(Environment)).fetchall() environments: list[Environment] = list(map(lambda x: x[0], result)) + print("[DEBUG] environments_get: found {} environment(s) in database".format(len(environments))) + return JSONResponse(status_code=200, content=[env.name for env in environments]) @@ -61,24 +64,30 @@ def environment_get(req: Request, name: str): """ db: Session = req.state.db + print("[DEBUG] environment_get: retrieving environment '{}'".format(name)) + # Validate name before query if not validate_environment_name(name): + print("[DEBUG] environment_get: ERROR - Invalid environment name '{}'. Environment names must use only alphanumeric, underscore or dash characters.".format(name)) raise HTTPException(status_code=400, detail="Invalid environment name format") stmt = select(Environment).where(Environment.name == name) result = db.execute(stmt).fetchall() if len(result) == 0: + print("[DEBUG] environment_get: ERROR - No environment found with name '{}'".format(name)) raise HTTPException(status_code=404, detail="No such environment exists") assert len(result) == 1 env: Environment = result[0][0] + print("[DEBUG] environment_get: resolved environment '{}' with id={}".format(env.name, env.id)) # Get assigned hosts count as an example of additional info hosts_stmt = select(Host).join(EnvironmentAssignment, Host.id == EnvironmentAssignment.host_id)\ .where(EnvironmentAssignment.environment_id == env.id) hosts_count = db.execute(hosts_stmt).fetchall().__len__() + print("[DEBUG] environment_get: environment '{}' has {} host(s) assigned".format(env.name, hosts_count)) return JSONResponse(status_code=200, content={ 'environment': env.name, @@ -103,8 +112,11 @@ def environment_create(req: Request, name: str): """ db = req.state.db + print("[DEBUG] environment_create: creating new environment '{}'".format(name)) + # Validate name format if not validate_environment_name(name): + print("[DEBUG] environment_create: ERROR - Invalid environment name '{}'. Environment names must use only alphanumeric, underscore or dash characters.".format(name)) raise HTTPException(status_code=400, detail="Invalid environment name. Use only alphanumeric, underscore or dash characters.") @@ -113,10 +125,12 @@ def environment_create(req: Request, name: str): existing = db.execute(stmt_check).fetchall() if len(existing) > 0: + print("[DEBUG] environment_create: ERROR - Environment '{}' already exists in database. Duplicate environment creation rejected.".format(name)) raise HTTPException(status_code=409, detail="Environment already exists") new_id = uuid.uuid4() db.execute(insert(Environment).values(id=new_id, name=name)) + print("[DEBUG] environment_create: created environment '{}' with id={}".format(name, new_id)) return JSONResponse(status_code=201, content={ 'id': str(new_id), @@ -143,19 +157,24 @@ def environment_delete(req: Request, name: str): """ db = req.state.db + print("[DEBUG] environment_delete: deleting environment '{}'".format(name)) + # Validate name format if not validate_environment_name(name): + print("[DEBUG] environment_delete: ERROR - Invalid environment name '{}'. Environment names must use only alphanumeric, underscore or dash characters.".format(name)) raise HTTPException(status_code=400, detail="Invalid environment name format") stmt = select(Environment).where(Environment.name == name) result = db.execute(stmt).fetchall() if len(result) == 0: + print("[DEBUG] environment_delete: ERROR - No environment found with name '{}'".format(name)) raise HTTPException(status_code=404, detail="No such environment exists") assert len(result) == 1 env: Environment = result[0][0] + print("[DEBUG] environment_delete: resolved environment '{}' with id={}".format(env.name, env.id)) # Check for assigned hosts before deleting assignments_stmt = select(EnvironmentAssignment).where( @@ -173,6 +192,7 @@ def environment_delete(req: Request, name: str): hosts_stmt = select(Host).where(Host.id.in_(host_ids)) hosts: list[Host] = list(map(lambda x: x[0], db.execute(hosts_stmt).fetchall())) + print("[DEBUG] environment_delete: ERROR - Cannot delete environment '{}' because it has {} host(s) assigned: {}. All hosts must be unassigned before deletion.".format(name, len(assignments), [h.name for h in hosts])) return JSONResponse(status_code=409, content={ 'message': "Cannot delete an environment that still has hosts assigned", 'assigned_hosts': [h.name for h in hosts] @@ -180,6 +200,7 @@ def environment_delete(req: Request, name: str): # Delete the environment db.execute(delete(Environment).where(Environment.id == env.id)) + print("[DEBUG] environment_delete: successfully deleted environment '{}' (id={})".format(env.name, env.id)) return JSONResponse(status_code=204, content={}) @@ -189,31 +210,35 @@ def environment_patch(req: Request, name: str) -> JSONResponse: db: Session = req.state.db cfg: Config = config() + print("[DEBUG] environment_patch: importing/syncing environment '{}' from git".format(name)) + # Attempt to check the requested branch out try: checkout_remote_branch(cfg, name) + print("[DEBUG] environment_patch: successfully checked out git branch '{}'".format(name)) # create the environment if it did not exist already select_env_res = db.execute(select(Environment).where(Environment.name == name)).fetchall() - print(select_env_res) if len(select_env_res) == 0: insert_env_res = db.execute(insert(Environment).values(id=uuid.uuid4(), name=name).returning(Environment)).fetchall() - print(insert_env_res) + print("[DEBUG] environment_patch: environment '{}' does not exist in database, creating new record".format(name)) if len(insert_env_res) == 0: + print("[DEBUG] environment_patch: ERROR - Failed to create environment '{}' in database after git checkout. Database insert returned no results.".format(name)) raise HTTPException(status_code=404, detail=f"Failed to create non-existent environment '{name}'") else: env: Environment = insert_env_res[0][0] else: env: Environment = select_env_res[0][0] + print("[DEBUG] environment_patch: environment '{}' already exists in database (id={})".format(name, env.id)) # Branch has been checked out - print(f"Reading states that are available in '{name}':") - print(f"{cfg.git.state_repo_path}:") all_files = recursive_list_dir(cfg.git.state_repo_path) sls_files = filter(lambda f: f.endswith(".sls"), all_files) state_file_paths = map(lambda x: x.replace("/init.sls", "").replace(".sls", ""), sls_files) state_names = list(map(lambda x: x.replace(f"{cfg.git.state_repo_path}/", "").replace("/", "."), state_file_paths)) + print("[DEBUG] environment_patch: found {} .sls file(s) in git repo '{}', resolving to {} state name(s)".format(len(all_files), cfg.git.state_repo_path, len(state_names))) + # get all the existing states and the to be created ones select_res = db.execute(select(State).where(State.name.in_(state_names))).fetchall() states_known = {} @@ -221,10 +246,14 @@ def environment_patch(req: Request, name: str) -> JSONResponse: state: State = row[0] states_known[state.name] = state + print("[DEBUG] environment_patch: {} of {} states already exist in database".format(len(states_known), len(state_names))) + states_new = { state_name: State(name=state_name, id=uuid.uuid4()) for state_name in state_names if state_name not in states_known } + print("[DEBUG] environment_patch: {} new state(s) to be created".format(len(states_new))) + states = {} states.update(states_known) states.update(states_new) @@ -242,14 +271,20 @@ def environment_patch(req: Request, name: str) -> JSONResponse: x[0].state_id for x in db.execute(select(StateAssignment).where(StateAssignment.environment_id == env.id)).fetchall() ] + print("[DEBUG] environment_patch: {} existing state assignment(s) found for environment '{}'".format(len(state_assignments_known), name)) + state_assignments_new = [ state_id for state_id in map(lambda x: x.id, states.values()) if state_id not in state_assignments_known ] + print("[DEBUG] environment_patch: {} new state assignment(s) to be created".format(len(state_assignments_new))) + state_assignments_to_delete = [ sid for sid in filter(lambda x: x not in state_ids, state_assignments_known) ] + if len(state_assignments_to_delete) > 0: + print("[DEBUG] environment_patch: {} stale state assignment(s) to be removed (states no longer exist in git)".format(len(state_assignments_to_delete))) delete_stmt = delete(StateAssignment) for sid in state_assignments_to_delete: @@ -261,5 +296,5 @@ def environment_patch(req: Request, name: str) -> JSONResponse: except Exception as exc: - print(f"Failed to import environment: {exc}") + print("[DEBUG] environment_patch: ERROR - Failed to import environment '{}': {}".format(name, str(exc))) raise HTTPException(status_code=404, detail=str(exc)) \ No newline at end of file diff --git a/pillar_tool/routers/host.py b/pillar_tool/routers/host.py index 477a965..46acd80 100644 --- a/pillar_tool/routers/host.py +++ b/pillar_tool/routers/host.py @@ -35,9 +35,12 @@ def hosts_get(req: Request): """ db: Session = req.state.db + print("[DEBUG] hosts_get: retrieving all hosts (excluding hostgroups)") result = db.execute(select(Host).where(Host.is_hostgroup == False)).fetchall() hosts: list[Host] = list(map(lambda x: x[0], result)) + print("[DEBUG] hosts_get: found {} host(s) in database".format(len(hosts))) + return JSONResponse(status_code=200, content=list(map(lambda x: x.name, hosts))) @@ -63,16 +66,21 @@ def host_get(req: Request, fqdn: str): """ db: Session = req.state.db + print("[DEBUG] host_get: retrieving host '{}'".format(fqdn)) + if not validate_fqdn(fqdn): + print("[DEBUG] host_get: ERROR - Provided FQDN '{}' is invalid. Host names must be valid identifiers.".format(fqdn)) raise HTTPException(status_code=400, detail="Provided host is not an FQDN") host_stmt = select(Host).where(Host.name == fqdn) result = db.execute(host_stmt).fetchall() if len(result) != 1: + print("[DEBUG] host_get: ERROR - Expected exactly one host with name '{}' but got {} results. This may indicate a database integrity violation (duplicate host names)".format(fqdn, len(result))) raise HTTPException(status_code=404, detail=f"No such host found (length of result was {len(result)})") host: Host = result[0][0] + print("[DEBUG] host_get: resolved host '{}' with id={}".format(host.name, host.id)) last_parent = host path = [] @@ -88,6 +96,7 @@ def host_get(req: Request, fqdn: str): last_parent = parent path.reverse() + print("[DEBUG] host_get: resolved hierarchical path for '{}': {}".format(fqdn, '/'.join(map(lambda x: x.name, path)))) return JSONResponse(status_code=200, content={ "host": host.name, "path": '/'.join(map(lambda x: x.name, path)) @@ -116,14 +125,19 @@ async def host_add(request: Request, fqdn: str, params: HostCreateParams): """ db: Session = request.state.db + print("[DEBUG] host_add: creating new host '{}'".format(fqdn)) + # Validate that the provided FQDN is properly formatted if not validate_fqdn(fqdn): + print("[DEBUG] host_add: ERROR - Provided FQDN '{}' is invalid. Host names must be valid identifiers.".format(fqdn)) raise HTTPException(status_code=400, detail="Provided host is not an FQDN") # Process parent path if provided if params.parent is not None: + print("[DEBUG] host_add: resolving parent path '{}'".format(params.parent)) parent_labels = split_and_validate_path(params.parent) if parent_labels is None: + print("[DEBUG] host_add: ERROR - Parent path '{}' could not be parsed or validated.".format(params.parent)) raise HTTPException(status_code=400, detail="Provided parent is not a valid path") else: parent_labels = [] @@ -135,12 +149,14 @@ async def host_add(request: Request, fqdn: str, params: HostCreateParams): result = db.execute(stmt_select_respecting_parent).fetchall() if len(result) == 0: + print("[DEBUG] host_add: ERROR - Parent '{}' does not exist at level '{}'. Cannot create host without valid parent hierarchy.".format(label, parent_id)) raise HTTPException(status_code=400, detail="Parent does not exist") # Note: this should be enforced by the database assert len(result) == 1 parent_id = result[0][0].id + print("[DEBUG] host_add: resolved parent '{}' (id={})".format(label, parent_id)) # Create new host with unique ID and hierarchical structure new_host = Host( @@ -152,6 +168,8 @@ async def host_add(request: Request, fqdn: str, params: HostCreateParams): stmt_create_host_with_parent = insert(Host).values(id=new_host.id, name=new_host.name, parent_id=new_host.parent_id, is_hostgroup=new_host.is_hostgroup) db.execute(stmt_create_host_with_parent) + print("[DEBUG] host_add: created new host '{}' with id={} at parent level {}".format(fqdn, new_host.id, parent_id)) + # Prepare response with creation details output = { "message": "Host created", @@ -194,15 +212,20 @@ async def host_delete(request: Request, fqdn: str): db: Session = request.state.db + print("[DEBUG] host_delete: deleting host '{}'".format(fqdn)) + if not validate_fqdn(fqdn): + print("[DEBUG] host_delete: ERROR - Provided FQDN '{}' is invalid. Host names must be valid identifiers.".format(fqdn)) raise HTTPException(status_code=400, detail="Provided host is not an FQDN") host_stmt = select(Host).where(and_(Host.name == fqdn, Host.is_hostgroup == False)) host_res = db.execute(host_stmt).fetchall() if len(host_res) != 1: + print("[DEBUG] host_delete: ERROR - Expected exactly one non-hostgroup with name '{}' but got {} results. This may indicate a database integrity violation (duplicate host names)".format(fqdn, len(host_res))) raise HTTPException(status_code=400, detail="Host not found") host: Host = host_res[0][0] + print("[DEBUG] host_delete: deleting host '{}' with id={}".format(host.name, host.id)) db.execute(delete(Host).where(Host.id == host.id)) return JSONResponse(status_code=204, content={"message": "Host deleted"}) diff --git a/pillar_tool/routers/hostgroup.py b/pillar_tool/routers/hostgroup.py index bccef02..bc39e8d 100644 --- a/pillar_tool/routers/hostgroup.py +++ b/pillar_tool/routers/hostgroup.py @@ -31,9 +31,12 @@ def hostgroups_get(req: Request): """ db: Session = req.state.db + print("[DEBUG] hostgroups_get: retrieving all host groups") result = db.execute(select(Host).where(Host.is_hostgroup == True)).fetchall() hosts: list[Host] = list(map(lambda x: x[0], result)) + print("[DEBUG] hostgroups_get: found {} host group(s) in database".format(len(hosts))) + all_hostgroups = { x.id: x for x in hosts } all_hostgroup_names = [] for host in hosts: @@ -42,6 +45,8 @@ def hostgroups_get(req: Request): ancestors.append(all_hostgroups[ancestors[-1].parent_id]) all_hostgroup_names.append('/'.join(map(lambda x: x.name, reversed(ancestors)))) + print("[DEBUG] hostgroups_get: resolved hierarchical names for {} host group(s): {}".format(len(all_hostgroup_names), all_hostgroup_names)) + return JSONResponse(status_code=200, content=all_hostgroup_names) @router.get("/{name}") @@ -63,6 +68,8 @@ def hostgroup_get(req: Request, name: str, params: HostgroupParams): """ db: Session = req.state.db + print("[DEBUG] hostgroup_get: retrieving hostgroup '{}'".format(name)) + # decode the path last = None ancestors = [] @@ -70,7 +77,8 @@ def hostgroup_get(req: Request, name: str, params: HostgroupParams): if params: path = split_and_validate_path(params.path) if params.path else [] - print("test") + if len(path) > 0: + print("[DEBUG] hostgroup_get: resolving parent path with {} segment(s): {}".format(len(path), path)) # get the path from the db path_stmt = select(Host).where(and_(Host.name == bindparam('name') and Host.parent_id == bindparam('parent_id'))) @@ -79,23 +87,29 @@ def hostgroup_get(req: Request, name: str, params: HostgroupParams): # error 404 if there is no matching item if len(result) != 1: + print("[DEBUG] hostgroup_get: ERROR - No hostgroup found with name '{}' at parent level '{}'. Path traversal failed.".format(label, last)) raise HTTPException(status_code=404, detail="No such hostgroup path exists") tmp: Host = result[0][0] ancestors.append(tmp) last = tmp.id + if len(ancestors) > 0: + print("[DEBUG] hostgroup_get: resolved parent path '{}'".format('/'.join(x.name for x in ancestors))) + # get the host in question stmt = select(Host).where(and_(Host.name == name, Host.is_hostgroup == True, Host.parent_id == last)) result = db.execute(stmt).fetchall() if len(result) == 0: + print("[DEBUG] hostgroup_get: ERROR - No hostgroup found with name '{}' and parent_id '{}'. This may indicate the hostgroup does not exist or has a different parent.".format(name, last)) raise HTTPException(status_code=404, detail="No such hostgroup exists") # Note: this should be enforced by the database assert len(result) == 1 hg: Host = result[0][0] + print("[DEBUG] hostgroup_get: resolved hostgroup '{}' with id={}".format(hg.name, hg.id)) return JSONResponse(status_code=200, content={ 'hostgroup': hg.name, @@ -124,6 +138,8 @@ def hostgroup_create(req: Request, name: str, params: HostgroupParams): labels = ( split_and_validate_path(path) if path is not None else [] ) or [] labels += [ name ] + print("[DEBUG] hostgroup_create: creating hostgroup hierarchy with {} label(s): {}".format(len(labels), labels)) + stmt = select(Host).where(and_(Host.name == bindparam('name'), Host.is_hostgroup == True, Host.parent_id == bindparam('last'))) last = None for label in labels: @@ -132,15 +148,19 @@ def hostgroup_create(req: Request, name: str, params: HostgroupParams): if len(result) == 1: # simply step down through the hierarchy host = result[0][0] + print("[DEBUG] hostgroup_create: existing hostgroup '{}' (id={}) found at parent level '{}', reusing".format(label, host.id, last)) last = host.id elif len(result) == 0: new_id = uuid.uuid4() db.execute(insert(Host).values(id=new_id, name=label, is_hostgroup=True, parent_id=last)) + print("[DEBUG] hostgroup_create: created new hostgroup '{}' with id={} at parent level {}".format(label, new_id, last)) last = new_id else: # this should not be possible + print("[DEBUG] hostgroup_create: ERROR - Multiple hostgroups found with name '{}' and parent '{}'. This indicates a database integrity violation (duplicate hostgroup names). Expected unique constraint enforcement.".format(label, last)) assert False + print("[DEBUG] hostgroup_create: successfully created/resolved hostgroup hierarchy ending at '{}'".format(labels[-1])) # TODO: return the newly created hostgroups return JSONResponse(status_code=201, content={}) @@ -168,26 +188,32 @@ def hostgroup_delete(req: Request, name: str, params: HostgroupParams = Depends( labels.append(name) last = None + print("[DEBUG] hostgroup_delete: deleting hostgroup hierarchy with {} label(s): {}".format(len(labels), labels)) + stmt_step = select(Host).where(and_(Host.name == bindparam('name'), Host.parent_id == bindparam('last'), Host.is_hostgroup == True)) for label in labels: result = db.execute(stmt_step, {'name': label, 'last': last}).fetchall() if len(result) == 0: + print("[DEBUG] hostgroup_delete: ERROR - No hostgroup found with name '{}' at parent level '{}'. Path traversal failed.".format(label, last)) return JSONResponse(status_code=404, content={}) # TODO: truly define a error format # this should be enforced by the database assert len(result) == 1 host: Host = result[0][0] + print("[DEBUG] hostgroup_delete: resolved hostgroup '{}' (id={}) at parent level {}".format(label, host.id, last)) last = host.id children_stmt = select(Host).where(Host.parent_id == last) children: list[Host] = list(map(lambda x: x[0], db.execute(children_stmt).fetchall())) if len(children) != 0: + print("[DEBUG] hostgroup_delete: ERROR - Cannot delete hostgroup '{}' because it has {} child(ren): {}. All children must be deleted first.".format(labels[-1], len(children), [ '/'.join(labels + [x.name]) for x in children ])) return JSONResponse(status_code=400, content={ 'message': "Cannot delete a hostgroup that still has children", 'children': [ '/'.join(labels + [x.name]) for x in children ] }) + print("[DEBUG] hostgroup_delete: deleting hostgroup '{}' (id={}) with no remaining children".format(labels[-1], last)) db.execute(delete(Host).where(Host.id == last)) return JSONResponse(status_code=204, content={}) diff --git a/pillar_tool/routers/pillar.py b/pillar_tool/routers/pillar.py index 72fc17f..a66c797 100644 --- a/pillar_tool/routers/pillar.py +++ b/pillar_tool/routers/pillar.py @@ -37,9 +37,12 @@ def pillar_get(req: Request, target: str) -> JSONResponse: # if any error happens, return non-200 status and an empty dictionary so that salt does not shit itself db: Session = req.state.db + print("[DEBUG] pillar_get: retrieving pillar data for target '{}'".format(target)) + # if the target is a hostgroup with path, then split the path and get to the target host this way target = target.replace("%%2F", "%%2f") if "%%2f" in target: + print("[DEBUG] pillar_get: target '{}' contains path separators (%%2f), resolving hierarchical path".format(target)) path_labels = target.split("%%2f") @@ -53,6 +56,7 @@ def pillar_get(req: Request, target: str) -> JSONResponse: result = db.execute(host_stmt, {"frag": fragment, "parent_id": parent_id}).fetchall() host_stmt = host_stmt_remain if len(result) == 0: + print("[DEBUG] pillar_get: ERROR - No host found with name '{}' and parent_id '{}'. Path resolution failed at this segment.".format(fragment, parent_id)) return JSONResponse(status_code=404, content={"message": f"No such path fragment: {fragment} with parent_id {parent_id}"}) assert len(result) == 1 # Note: that the db should enforce this @@ -62,13 +66,16 @@ def pillar_get(req: Request, target: str) -> JSONResponse: else: + print("[DEBUG] pillar_get: target '{}' is a single name, resolving host hierarchy".format(target)) # get the host hierarchy from a fqdn or unique hostgroup name host_stmt = select(Host).where(Host.name == target) result = db.execute(host_stmt).fetchall() if len(result) == 0: + print("[DEBUG] pillar_get: ERROR - No host found with name '{}'".format(target)) return JSONResponse(status_code=404, content={'message': f'No such target: {target}'}) # NOTE: should be enforced by the database if len(result) > 1: + print("[DEBUG] pillar_get: ERROR - Multiple hosts found with name '{}'. This indicates a database integrity violation (duplicate host names).".format(target)) return JSONResponse(status_code=400, content={'message': f'Multiple targets: {target}'}) host: Host = result[0][0] @@ -83,8 +90,10 @@ def pillar_get(req: Request, target: str) -> JSONResponse: path.reverse() + print("[DEBUG] pillar_get: resolved host hierarchy with {} hosts: {}".format(len(path), [h.name for h in path])) out = merge([get_pillar_for_target(db, host.id) for host in path]) + print("[DEBUG] pillar_get: merged pillar data contains {} top-level key(s)".format(len(out))) return JSONResponse(status_code=200, content=out) @@ -93,8 +102,11 @@ def pillar_get(req: Request, target: str) -> JSONResponse: def pillar_create(req: Request, name: str, params: PillarParams): db: Session = req.state.db + print("[DEBUG] pillar_create: creating pillar '{}' for host/hostgroup={}/hostgroup={}".format(name, params.host, params.hostgroup)) + # ensure that value and type have been set in the request parameters if params.type is None or params.value is None: + print("[DEBUG] pillar_create: ERROR - Both parameter type and value must be set. Received type={}, value={}".format(params.type, params.value)) return JSONResponse(status_code=400, content={ 'message': "Both parameter type and value need to be set!" }) @@ -103,18 +115,22 @@ def pillar_create(req: Request, name: str, params: PillarParams): pillar_data = validate_pillar_input_data(params.value, params.type) if params.host is not None: + print("[DEBUG] pillar_create: targeting host '{}'".format(params.host)) target_stmt = select(Host).where(Host.name == params.host) result = db.execute(target_stmt).fetchall() if len(result) == 0: + print("[DEBUG] pillar_create: ERROR - Host '{}' not found in database".format(params.host)) return JSONResponse(status_code=404, content={}) # this should be enforced by the database assert len(result) == 1 target: Host = result[0][0] elif params.hostgroup is not None: + print("[DEBUG] pillar_create: targeting hostgroup '{}'".format(params.hostgroup)) path = split_and_validate_path(params.hostgroup) if not path: + print("[DEBUG] pillar_create: ERROR - Hostgroup path '{}' could not be parsed or validated.".format(params.hostgroup)) return JSONResponse(status_code=400, content={'message': "No target specified"}) last = None current = None @@ -124,6 +140,7 @@ def pillar_create(req: Request, name: str, params: PillarParams): for label in path: result = db.execute(group_stmt if last is not None else group_stmt_none, {'name': label, 'parent': last}).fetchall() if len(result) == 0: + print("[DEBUG] pillar_create: ERROR - No hostgroup found with name '{}' at parent level '{}'. Path traversal failed.".format(label, last)) return JSONResponse(status_code=404, content={'message': f"No hostgroup named: {params.hostgroup}"}) # Note: this should be enforced by the database assert len(result) == 1, f"Result: {[x[0].name for x in result]}" @@ -131,6 +148,7 @@ def pillar_create(req: Request, name: str, params: PillarParams): last = current.id target: Host = current else: + print("[DEBUG] pillar_create: ERROR - Neither host nor hostgroup specified in request parameters.") return JSONResponse(status_code=400, content={'message': "Neither host nor hostgroup set"}) # if this is a dictionary value, parse it and create a separate entry for all the sub-pillars @@ -150,6 +168,7 @@ def pillar_create(req: Request, name: str, params: PillarParams): return out pillars_to_store = aux(name, pillar_data) + print("[DEBUG] pillar_create: dictionary value expanded into {} sub-pillar entries".format(len(pillars_to_store))) else: # build the pillar package @@ -157,6 +176,8 @@ def pillar_create(req: Request, name: str, params: PillarParams): { 'name': name, 'value': params.value, 'type': params.type } ] + print("[DEBUG] pillar_create: storing {} pillar entry/entries for target '{}' (id={})".format(len(pillars_to_store), target.name, target.id)) + # store pillar data insert_stmt = insert(Pillar).values(id=bindparam('new_id'), host_id=target.id, pillar_name=bindparam('name'), parameter_type=bindparam('type'), value=bindparam('value')) upsert_stmt = insert_stmt.on_conflict_do_update(constraint='pillar_unique_pillar_name', set_={'parameter_type': bindparam('type'), 'value': bindparam('value')} ) @@ -165,6 +186,7 @@ def pillar_create(req: Request, name: str, params: PillarParams): instance['new_id'] = uuid4() result = db.execute(upsert_stmt, instance) + print("[DEBUG] pillar_create: successfully stored pillar '{}'".format(name)) return JSONResponse(status_code=200, content={'message': 'ok'}) @@ -175,12 +197,16 @@ def pillar_create(req: Request, name: str, params: PillarParams): def pillar_delete(req: Request, name: str, params: PillarParams): db = req.state.db + print("[DEBUG] pillar_delete: deleting pillar '{}' for host/hostgroup={}/hostgroup={}".format(name, params.host, params.hostgroup)) + if params.host is not None: # delete a pillar at the host level + print("[DEBUG] pillar_delete: targeting host '{}'".format(params.host)) target_stmt = select(Host).where(and_(Host.name == params.host, Host.is_hostgroup == False)) result = db.execute(target_stmt).fetchall() if len(result) == 0: + print("[DEBUG] pillar_delete: ERROR - Host '{}' not found in database".format(params.host)) return JSONResponse(status_code=404, content={}) # this should be enforced by the database @@ -188,8 +214,10 @@ def pillar_delete(req: Request, name: str, params: PillarParams): target: Host = result[0][0] elif params.hostgroup is not None: # delete a pillar at the hostgroup level + print("[DEBUG] pillar_delete: targeting hostgroup '{}'".format(params.hostgroup)) path = split_and_validate_path(params.hostgroup) if not path: + print("[DEBUG] pillar_delete: ERROR - Hostgroup path '{}' could not be parsed or validated.".format(params.hostgroup)) return JSONResponse(status_code=400, content={'message': "No target specified"}) last = None current = None @@ -199,6 +227,7 @@ def pillar_delete(req: Request, name: str, params: PillarParams): for label in path: result = db.execute(group_stmt if last is not None else group_stmt_none, {'name': label, 'parent': last}).fetchall() if len(result) == 0: + print("[DEBUG] pillar_delete: ERROR - No hostgroup found with name '{}' at parent level '{}'. Path traversal failed.".format(label, last)) return JSONResponse(status_code=404, content={'message': f"No hostgroup named: {params.hostgroup}"}) # Note: this should be enforced by the database assert len(result) == 1, f"Result: {[x[0].name for x in result]}" @@ -206,11 +235,15 @@ def pillar_delete(req: Request, name: str, params: PillarParams): last = current.id target: Host = current else: + print("[DEBUG] pillar_delete: ERROR - Neither host nor hostgroup specified in request parameters.") return JSONResponse(status_code=400, content={ 'message': "Either Host or Hostgroup needs to be set!" }) + print("[DEBUG] pillar_delete: deleting pillar '{}' (and sub-pillars '{}.*') from target '{}' (id={})".format(name, name, target.name, target.id)) + delete_stmt = delete(Pillar).where(and_(Pillar.host_id == target.id, or_(Pillar.pillar_name == name, Pillar.pillar_name.like(f"{name}:%")))) result = db.execute(delete_stmt) + print("[DEBUG] pillar_delete: successfully deleted pillar '{}' from target '{}'".format(name, target.name)) return JSONResponse(status_code=200, content={'message': 'ok'}) diff --git a/pillar_tool/routers/state.py b/pillar_tool/routers/state.py index 99c9bba..b1067d7 100644 --- a/pillar_tool/routers/state.py +++ b/pillar_tool/routers/state.py @@ -30,9 +30,12 @@ def states_get(req: Request): """ db: Session = req.state.db + print("[DEBUG] states_get: retrieving all states") result = db.execute(select(State)).fetchall() states: list[State] = list(map(lambda x: x[0], result)) + print("[DEBUG] states_get: found {} state(s) in database".format(len(states))) + return JSONResponse(status_code=200, content=[state.name for state in states]) @@ -54,25 +57,31 @@ def state_get(req: Request, name: str): """ db: Session = req.state.db + print("[DEBUG] state_get: retrieving state '{}'".format(name)) + # Validate name before query if not validate_state_name(name): + print("[DEBUG] state_get: ERROR - Invalid state name format '{}'. State names must start with a letter or underscore and contain only alphanumeric characters, underscores, or dashes.".format(name)) raise HTTPException(status_code=400, detail="Invalid state name format") stmt = select(State).where(State.name == name) result = db.execute(stmt).fetchall() if len(result) == 0: + print("[DEBUG] state_get: ERROR - No state found with name '{}'".format(name)) raise HTTPException(status_code=404, detail="No such state exists") assert len(result) == 1 state: State = result[0][0] + print("[DEBUG] state_get: resolved state '{}' with id={}".format(state.name, state.id)) # Get assigned hosts count as an example of additional info assignments_stmt = select(StateAssignment).where( StateAssignment.state_id == state.id ) assignments_count = db.execute(assignments_stmt).fetchall().__len__() + print("[DEBUG] state_get: state '{}' has {} environment assignment(s)".format(state.name, assignments_count)) return JSONResponse(status_code=200, content={ 'state': state.name, @@ -97,8 +106,11 @@ def state_create(req: Request, name: str, patch_params: StateParams): """ db = req.state.db + print("[DEBUG] state_create: creating new state '{}'".format(name)) + # Validate name format if not validate_state_name(name): + print("[DEBUG] state_create: ERROR - Invalid state name '{}'. State names must start with a letter or underscore and contain only alphanumeric characters, underscores, or dashes.".format(name)) raise HTTPException(status_code=400, detail="Invalid state name. State names must start with a letter or underscore and contain only alphanumeric characters, underscores, or dashes.") @@ -107,20 +119,24 @@ def state_create(req: Request, name: str, patch_params: StateParams): existing = db.execute(stmt_check).fetchall() if len(existing) > 0: + print("[DEBUG] state_create: ERROR - State '{}' already exists in database. Duplicate state creation rejected.".format(name)) raise HTTPException(status_code=409, detail="State already exists") new_id = uuid.uuid4() db.execute(insert(State).values(id=new_id, name=name)) + print("[DEBUG] state_create: created state '{}' with id={}".format(name, new_id)) stmt_set_env = insert(StateAssignment).values(state_id=new_id, environment_id=bindparam('env_id')) stmt_get_env_id = select(Environment).where(Environment.name == bindparam('env_name')) for env in patch_params.addenv: env_id_res = db.execute(stmt_get_env_id, {'env_name': env}).fetchall() if len(env_id_res) < 1: + print("[DEBUG] state_create: ERROR - Environment '{}' does not exist. Cannot assign non-existent environment to new state.".format(env)) raise HTTPException(status_code=404, detail="No such environment exists") env_id = env_id_res[0][0].id db.execute(stmt_set_env, {'env_id': env_id}) + print("[DEBUG] state_create: assigned state '{}' to environment '{}' (id={})".format(name, env, env_id)) return JSONResponse(status_code=201, content={ 'id': str(new_id), @@ -147,19 +163,24 @@ def state_delete(req: Request, name: str): """ db = req.state.db + print("[DEBUG] state_delete: deleting state '{}'".format(name)) + # Validate name format if not validate_state_name(name): + print("[DEBUG] state_delete: ERROR - Invalid state name '{}'. State names must start with a letter or underscore and contain only alphanumeric characters, underscores, or dashes.".format(name)) raise HTTPException(status_code=400, detail="Invalid state name format") stmt = select(State).where(State.name == name) result = db.execute(stmt).fetchall() if len(result) == 0: + print("[DEBUG] state_delete: ERROR - No state found with name '{}'".format(name)) raise HTTPException(status_code=404, detail="No such state exists") assert len(result) == 1 state: State = result[0][0] + print("[DEBUG] state_delete: resolved state '{}' with id={}".format(state.name, state.id)) # Check for assigned environments before deleting assignments_stmt = select(StateAssignment).where( @@ -177,6 +198,7 @@ def state_delete(req: Request, name: str): envs_stmt = select(Environment).where(Environment.id.in_(env_ids)) envs: list[Environment] = list(map(lambda x: x[0], db.execute(envs_stmt).fetchall())) + print("[DEBUG] state_delete: ERROR - Cannot delete state '{}' because it has {} environment assignment(s): {}. State must be unassigned from all environments before deletion.".format(name, len(assignments), [e.name for e in envs])) return JSONResponse(status_code=409, content={ 'message': "Cannot delete a state that still has environment assignments", 'assigned_envs': [e.name for e in envs] @@ -196,6 +218,7 @@ def state_delete(req: Request, name: str): hosts_stmt = select(Host).where(Host.id.in_(host_ids)) hosts: list[Host] = list(map(lambda x: x[0], db.execute(hosts_stmt).fetchall())) + print("[DEBUG] state_delete: ERROR - Cannot delete state '{}' because it has {} host assignment(s): {}. State must be unassigned from all hosts before deletion.".format(name, len(top), [h.name for h in hosts])) return JSONResponse(status_code=409, content={ 'message': "Cannot delete a state that still has host assignments", 'assigned_hosts': [h.name for h in hosts] @@ -203,6 +226,7 @@ def state_delete(req: Request, name: str): # Delete the state db.execute(delete(State).where(State.id == state.id)) + print("[DEBUG] state_delete: successfully deleted state '{}' (id={})".format(state.name, state.id)) return JSONResponse(status_code=204, content={}) @@ -211,11 +235,15 @@ def state_patch(req: Request, name: str, patch_params: StateParams): db: Session = req.state.db + print("[DEBUG] state_patch: patching state '{}' with addenv={}, delenv={}".format(name, patch_params.addenv, patch_params.delenv)) + stmt_state_id = select(State).where(State.name == name) selected_state_res = db.execute(stmt_state_id).fetchall() if len(selected_state_res) != 1: + print("[DEBUG] state_patch: ERROR - No state found with name '{}'".format(name)) raise HTTPException(status_code=404, detail="No such state exists") state: State = selected_state_res[0][0] + print("[DEBUG] state_patch: resolved state '{}' with id={}".format(state.name, state.id)) # Statement for getting the stmt_get_env_id = select(Environment).where(Environment.name == bindparam('env_name')) @@ -225,18 +253,22 @@ def state_patch(req: Request, name: str, patch_params: StateParams): for env in patch_params.addenv: env_id_res = db.execute(stmt_get_env_id, {'env_name': env}).fetchall() if len(env_id_res) < 1: + print("[DEBUG] state_patch: ERROR - Environment '{}' does not exist. Cannot add non-existent environment to state.".format(env)) raise HTTPException(status_code=404, detail="No such environment exists") env_id = env_id_res[0][0].id db.execute(stmt_set_env, {'env_id': env_id}) + print("[DEBUG] state_patch: added state '{}' to environment '{}' (id={})".format(name, env, env_id)) stmt_del_env = delete(StateAssignment).where(and_(StateAssignment.state_id == state.id, StateAssignment.environment_id == bindparam('env_id'))) for env in patch_params.delenv: env_id_res = db.execute(stmt_get_env_id, {'env_name': env}).fetchall() if len(env_id_res) < 1: + print("[DEBUG] state_patch: ERROR - Environment '{}' does not exist. Cannot remove non-existent environment from state.".format(env)) raise HTTPException(status_code=404, detail="No such environment exists") env_id = env_id_res[0][0].id db.execute(stmt_del_env, {'env_id': env_id}) + print("[DEBUG] state_patch: removed state '{}' from environment '{}' (id={})".format(name, env, env_id)) return JSONResponse(status_code=204, content={}) \ No newline at end of file diff --git a/pillar_tool/routers/top.py b/pillar_tool/routers/top.py index 4a386cf..4fbf056 100644 --- a/pillar_tool/routers/top.py +++ b/pillar_tool/routers/top.py @@ -22,15 +22,20 @@ router = APIRouter( def top_get(req: Request, host: str): db: Session = req.state.db + print("[DEBUG] top_get: querying top file assignments for host '{}'".format(host)) + # build the hierarchy host_stmt = select(Host).where(Host.name == host) result = db.execute(host_stmt).fetchall() if len(result) == 0: + print("[DEBUG] top_get: ERROR - Host '{}' not found in database".format(host)) return JSONResponse(status_code=404, content={"message": "Host '{}' not found".format(host)}) elif len(result) > 1: + print("[DEBUG] top_get: ERROR - More than one host found with name '{}'. This indicates a database integrity violation (duplicate host names). Expected unique constraint enforcement.".format(host)) return JSONResponse(status_code=500, content={"message": "More than one host found"}) else: target_host: Host = result[0][0] + print("[DEBUG] top_get: resolved host '{}' with id={}".format(host, target_host.id)) parent_stmt = select(Host).where(Host.id == bindparam("parent_id")) parents = [] @@ -39,14 +44,18 @@ def top_get(req: Request, host: str): parents.append(current) result = db.execute(parent_stmt, {'parent_id': current.parent_id}).fetchall() if len(result) == 0: + print("[DEBUG] top_get: WARNING - parent_id '{}' for host '{}' does not exist in database. Hierarchy traversal stopped.".format(current.parent_id, current.name)) current = None elif len(result) > 1: + print("[DEBUG] top_get: ERROR - More than one parent found for host '{}'. This indicates a database integrity violation (multiple parents for single host).".format(current.name)) return JSONResponse(status_code=500, content={"message": "More than one parent host found"}) else: current = result[0][0] if current is not None: parents.append(current) + print("[DEBUG] top_get: resolved parent hierarchy with {} hosts: {}".format(len(parents), [p.name for p in parents])) + env_stmt = (select(Environment) .join(EnvironmentAssignment, EnvironmentAssignment.environment_id == Environment.id) .where(EnvironmentAssignment.host_id == bindparam("host_id")) @@ -57,6 +66,9 @@ def top_get(req: Request, host: str): if len(env_res) == 1: env: Environment = env_res[0][0] + if env is None: + print("[DEBUG] top_get: WARNING - No environment assigned to host '{}' or any of its ancestors in the hierarchy".format(host)) + state_stmt = (select(State) .join(TopFile, State.id == TopFile.state_id) .join(StateAssignment, State.id == StateAssignment.state_id) @@ -70,6 +82,8 @@ def top_get(req: Request, host: str): all_assigned_states = set(s for states in assigned_states for s in states) env_name = env.name + print("[DEBUG] top_get: found {} assigned state(s) for host '{}' in environment '{}': {}".format(len(all_assigned_states), host, env.name, [s.name for s in all_assigned_states])) + return JSONResponse(status_code=200, content={ env.name: list(map(lambda state: state.name, all_assigned_states)), }) @@ -79,31 +93,36 @@ def top_get(req: Request, host: str): def top_setenv(req: Request, host: str, environment: str): db: Session = req.state.db + print("[DEBUG] top_setenv: assigning environment '{}' to host '{}'".format(environment, host)) + # get the target host id host_stmt = select(Host).where(Host.name == host) host_res = db.execute(host_stmt).fetchall() if len(host_res) == 0: + print("[DEBUG] top_setenv: ERROR - Host '{}' not found in database".format(host)) return JSONResponse(status_code=404, content={"error": "No host found"}) elif len(host_res) == 1: host_res = host_res[0][0] else: - # Note that this should be prevented by the database + print("[DEBUG] top_setenv: ERROR - Too many hosts found with name '{}'. This should be prevented by a unique constraint on the database.".format(host)) return JSONResponse(status_code=404, content={"error": "Too many hosts found??? This should not happen"}) # get the environment id env_stmt = select(Environment).where(Environment.name == environment) env_res = db.execute(env_stmt).fetchall() if len(env_res) == 0: + print("[DEBUG] top_setenv: ERROR - Environment '{}' not found in database".format(environment)) return JSONResponse(status_code=404, content={"error": "No environment found"}) elif len(env_res) == 1: env_res = env_res[0][0] else: - # Note that this should be prevented by the database + print("[DEBUG] top_setenv: ERROR - Too many environments found with name '{}'. This should be prevented by a unique constraint on the database.".format(environment)) return JSONResponse(status_code=404, content={"error": "Too many environments found??? This should not happen"}) insert_stmt = insert(EnvironmentAssignment).values(environment_id=env_res.id, host_id=host_res.id) result = db.execute(insert_stmt) + print("[DEBUG] top_setenv: successfully assigned environment '{}' (id={}) to host '{}' (id={})".format(environment, env_res.id, host, host_res.id)) return JSONResponse(status_code=200, content={}) @@ -111,6 +130,8 @@ def top_setenv(req: Request, host: str, environment: str): def top_state_assign(req: Request, host_name: str, state_name: str): db: Session = req.state.db + print("[DEBUG] top_state_assign: assigning state '{}' to host '{}'".format(state_name, host_name)) + # get the host in question path_labels = host_name.replace("%%2F", "%%2f").split("%%2f") parent_id = None @@ -119,12 +140,14 @@ def top_state_assign(req: Request, host_name: str, state_name: str): host_res = db.execute(host_stmt).fetchall() if len(host_res) != 1: + print("[DEBUG] top_state_assign: ERROR - Host '{}' not found at path level '{}'. Expected exactly one match but got {} results.".format(host_name, path, len(host_res))) return JSONResponse(status_code=404, content={"error": f"Host '{host_name} not found"}) current: Host = host_res[0][0] parent_id = current.id host: Host = current + print("[DEBUG] top_state_assign: resolved target host '{}' with id={}".format(host.name, host.id)) parent_stmt = select(Host).where(Host.id == bindparam("parent_id")) parents: list[Host] = [] @@ -136,10 +159,13 @@ def top_state_assign(req: Request, host_name: str, state_name: str): else: parent = db.execute(parent_stmt, {'parent_id': current.parent_id}).fetchall() if len(parent) == 0: + print("[DEBUG] top_state_assign: ERROR - Host Hierarchy seems broken: host '{}' has parent_id '{}' which does not exist in database. This indicates a foreign key constraint violation or orphaned record.".format(current.name, current.parent_id)) return JSONResponse(status_code=500, content={"error": f"Host Hierarchy seems broken: parent_id '{current.parent_id}' does not exist"}) # Note: more than one result is impossible, since the id is a primary key current: Host = parent[0][0] + print("[DEBUG] top_state_assign: resolved parent hierarchy with {} hosts".format(len(parents))) + # get the hosts environment env_assign_stmt = select(EnvironmentAssignment).where(EnvironmentAssignment.host_id == bindparam("host_id")) env_assign: EnvironmentAssignment | None = None @@ -149,9 +175,14 @@ def top_state_assign(req: Request, host_name: str, state_name: str): env_assign: EnvironmentAssignment = env_res[0][0] break + if env_assign is None: + print("[DEBUG] top_state_assign: ERROR - Host '{}' has no environment assigned. A state assignment requires an environment context.".format(host_name)) + return JSONResponse(status_code=404, content={"error": f"Host '{host_name}' has no environment assigned"}) + env_stmt = select(Environment).where(Environment.id == env_assign.environment_id) env_res = db.execute(env_stmt).fetchall() if len(env_res) != 1: + print("[DEBUG] top_state_assign: ERROR - Environment id '{}' referenced by assignment does not exist in database. This indicates a foreign key constraint violation.".format(env_assign.environment_id)) return JSONResponse(status_code=404, content={"error": f"Host '{host_name}' has no environment assigned"}) env: Environment = env_res[0][0] @@ -161,11 +192,13 @@ def top_state_assign(req: Request, host_name: str, state_name: str): ) state_res = db.execute(state_stmt).fetchall() if len(state_res) != 1: + print("[DEBUG] top_state_assign: ERROR - No state '{}' found in environment '{}'. The state must exist and be assigned to the target environment.".format(state_name, env.name)) return JSONResponse(status_code=404, content={"error": f"No state '{state_name}' found in environment '{env.name}'"}) state: State = state_res[0][0] # insert the relation into the database db.execute(insert(TopFile).on_conflict_do_nothing('pillar_tool_top_file_unique_state_host').values(state_id=state.id, host_id=host.id)) + print("[DEBUG] top_state_assign: successfully assigned state '{}' to host '{}' in environment '{}'".format(state_name, host_name, env.name)) return JSONResponse(status_code=200, content={}) @@ -174,13 +207,17 @@ def top_state_assign(req: Request, host_name: str, state_name: str): def top_state_unassign(req: Request, host_name: str, state_name: str): db: Session = req.state.db + print("[DEBUG] top_state_unassign: removing assignment of state '{}' from host '{}'".format(state_name, host_name)) + # get the host in question host_stmt = select(Host).where(Host.name == host_name) host_res = db.execute(host_stmt).fetchall() if len(host_res) != 1: + print("[DEBUG] top_state_unassign: ERROR - Host '{}' not found. Expected exactly one match but got {} results.".format(host_name, len(host_res))) return JSONResponse(status_code=404, content={"error": f"Host '{host_name} not found"}) host: Host = host_res[0][0] + print("[DEBUG] top_state_unassign: resolved target host '{}' with id={}".format(host.name, host.id)) parent_stmt = select(Host).where(Host.id == bindparam("parent_id")) parents: list[Host] = [] @@ -192,10 +229,13 @@ def top_state_unassign(req: Request, host_name: str, state_name: str): else: parent = db.execute(parent_stmt, {'parent_id': current.parent_id}).fetchall() if len(parent) == 0: + print("[DEBUG] top_state_unassign: ERROR - Host Hierarchy seems broken: host '{}' has parent_id '{}' which does not exist in database. This indicates a foreign key constraint violation or orphaned record.".format(current.name, current.parent_id)) return JSONResponse(status_code=500, content={"error": f"Host Hierarchy seems broken: parent_id '{current.parent_id}' does not exist"}) # Note: more than one result is impossible, since the id is a primary key current: Host = parent[0][0] + print("[DEBUG] top_state_unassign: resolved parent hierarchy with {} hosts".format(len(parents))) + # get the hosts environment env_assign_stmt = select(EnvironmentAssignment).where(EnvironmentAssignment.host_id == bindparam("host_id")) env_assign: EnvironmentAssignment | None = None @@ -205,9 +245,14 @@ def top_state_unassign(req: Request, host_name: str, state_name: str): env_assign: EnvironmentAssignment = env_res[0][0] break + if env_assign is None: + print("[DEBUG] top_state_unassign: ERROR - Host '{}' has no environment assigned. Cannot unassign state without an environment context.".format(host_name)) + return JSONResponse(status_code=404, content={"error": f"Host '{host_name}' has no environment assigned"}) + env_stmt = select(Environment).where(Environment.id == env_assign.environment_id) env_res = db.execute(env_stmt).fetchall() if len(env_res) != 1: + print("[DEBUG] top_state_unassign: ERROR - Environment id '{}' referenced by assignment does not exist in database. This indicates a foreign key constraint violation.".format(env_assign.environment_id)) return JSONResponse(status_code=404, content={"error": f"Host '{host_name}' has no environment assigned"}) env: Environment = env_res[0][0] @@ -217,11 +262,13 @@ def top_state_unassign(req: Request, host_name: str, state_name: str): ) state_res = db.execute(state_stmt).fetchall() if len(state_res) != 1: + print("[DEBUG] top_state_unassign: ERROR - No state '{}' found in environment '{}'. The state must exist and be assigned to the target environment.".format(state_name, env.name)) return JSONResponse(status_code=404, content={"error": f"No state '{state_name}' found in environment '{env.name}'"}) state: State = state_res[0][0] # delete the relation from the database db.execute(delete(TopFile).where(and_(TopFile.state_id == state.id, TopFile.host_id == host.id))) + print("[DEBUG] top_state_unassign: successfully removed assignment of state '{}' from host '{}' in environment '{}'".format(state_name, host_name, env.name)) return JSONResponse(status_code=200, content={})