added some logging
This commit is contained in:
parent
e088bc47c0
commit
a9937380b0
@ -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))
|
||||
@ -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"})
|
||||
|
||||
@ -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={})
|
||||
|
||||
@ -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'})
|
||||
|
||||
@ -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={})
|
||||
@ -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={})
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user