Initial curated Unraid-AppOps-Gateway source from 3bd6f6fee310125bfbb4d4650389a817f498724d
This commit is contained in:
commit
1dfbca08a1
528 files changed
+131054
No files matched your search
@@ -0,0 +1,363 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sqlmodel import Session
|
||||
|
||||
from ..connectors.unraid import UnraidConnector
|
||||
from .approval_service import create_approval
|
||||
from .audit_logger import log_event
|
||||
from .job_runner import complete_job, create_job, set_job_progress, start_job
|
||||
from .policy_engine import evaluate_policy
|
||||
from .setup_service import apply_stored_connector_settings
|
||||
|
||||
|
||||
VM_ACTIONS: dict[str, dict[str, Any]] = {
|
||||
'start': {
|
||||
'action_id': 'unraid.vm.start',
|
||||
'expected_before': {'SHUTOFF', 'STOPPED', 'OFFLINE'},
|
||||
'expected_after': {'RUNNING', 'STARTED', 'ONLINE'},
|
||||
'risk': 'medium',
|
||||
'label': 'Start VM',
|
||||
},
|
||||
'resume': {
|
||||
'action_id': 'unraid.vm.resume',
|
||||
'expected_before': {'PAUSED', 'PMSUSPENDED'},
|
||||
'expected_after': {'RUNNING', 'STARTED', 'ONLINE'},
|
||||
'risk': 'medium',
|
||||
'label': 'Resume VM',
|
||||
},
|
||||
'pause': {
|
||||
'action_id': 'unraid.vm.pause',
|
||||
'expected_before': {'RUNNING', 'STARTED', 'ONLINE'},
|
||||
'expected_after': {'PAUSED'},
|
||||
'risk': 'high',
|
||||
'label': 'Pause VM',
|
||||
},
|
||||
'stop': {
|
||||
'action_id': 'unraid.vm.stop',
|
||||
'risk': 'critical',
|
||||
'label': 'Stop VM',
|
||||
'blocked_reason': (
|
||||
'The current Unraid stop mutation can force-stop a VM after its graceful-shutdown timeout. '
|
||||
'AppOps keeps this destructive fallback blocked.'
|
||||
),
|
||||
},
|
||||
'reboot': {
|
||||
'action_id': 'unraid.vm.reboot',
|
||||
'risk': 'critical',
|
||||
'label': 'Reboot VM',
|
||||
'blocked_reason': 'VM reboot remains blocked until guest-shutdown and recovery semantics can be verified safely.',
|
||||
},
|
||||
'force_stop': {
|
||||
'action_id': 'unraid.vm.force_stop',
|
||||
'risk': 'critical',
|
||||
'label': 'Force-stop VM',
|
||||
'blocked_reason': 'Force-stop is destructive and blocked by default.',
|
||||
},
|
||||
'reset': {
|
||||
'action_id': 'unraid.vm.reset',
|
||||
'risk': 'critical',
|
||||
'label': 'Reset VM',
|
||||
'blocked_reason': 'VM reset is destructive and blocked by default.',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _find_vm(connector: UnraidConnector, vm_id: str) -> tuple[dict[str, Any] | None, dict[str, Any]]:
|
||||
inventory = connector.get_vm_summary()
|
||||
if inventory.get('status') != 'ok':
|
||||
return None, inventory
|
||||
vm = next((item for item in inventory.get('vms', []) if str(item.get('id')) == vm_id), None)
|
||||
return vm, inventory
|
||||
|
||||
|
||||
def vm_capabilities(session: Session) -> dict[str, Any]:
|
||||
actions = []
|
||||
for action, definition in VM_ACTIONS.items():
|
||||
action_id = str(definition['action_id'])
|
||||
policy = evaluate_policy(session, action=action_id, target='*', source='manual')
|
||||
blocked_reason = definition.get('blocked_reason')
|
||||
actions.append({
|
||||
'id': action,
|
||||
'actionId': action_id,
|
||||
'label': definition['label'],
|
||||
'risk': definition['risk'],
|
||||
'policyDecision': 'blocked' if blocked_reason else policy['decision'],
|
||||
'reason': blocked_reason or policy['reason'],
|
||||
'executionSupported': action in {'start', 'resume', 'pause'},
|
||||
})
|
||||
return {
|
||||
'source': 'unraid_graphql',
|
||||
'writePermission': 'vm:update or vm:*',
|
||||
'actions': actions,
|
||||
}
|
||||
|
||||
|
||||
def request_vm_action(
|
||||
session: Session,
|
||||
*,
|
||||
vm_id: str,
|
||||
action: str,
|
||||
source: str = 'manual',
|
||||
confirmed: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
normalized_action = str(action).strip().lower()
|
||||
definition = VM_ACTIONS.get(normalized_action)
|
||||
if not definition:
|
||||
return {
|
||||
'success': False,
|
||||
'status': 'blocked',
|
||||
'result': 'blocked',
|
||||
'decision': 'blocked',
|
||||
'message': 'Unknown VM action.',
|
||||
}
|
||||
|
||||
action_id = str(definition['action_id'])
|
||||
risk = str(definition['risk'])
|
||||
blocked_reason = definition.get('blocked_reason')
|
||||
if blocked_reason:
|
||||
audit = log_event(
|
||||
session,
|
||||
source=source,
|
||||
identity='operator',
|
||||
action_id=action_id,
|
||||
target=vm_id,
|
||||
target_type='virtual_machine',
|
||||
risk=risk,
|
||||
policy_decision='blocked',
|
||||
reason=str(blocked_reason),
|
||||
result='blocked',
|
||||
execution_plan={'steps': []},
|
||||
redacted_metadata={'vmId': vm_id, 'requestedAction': normalized_action},
|
||||
)
|
||||
return {
|
||||
'success': False,
|
||||
'status': 'blocked',
|
||||
'result': 'blocked',
|
||||
'decision': 'blocked',
|
||||
'policy_decision': 'blocked',
|
||||
'message': blocked_reason,
|
||||
'auditId': audit.id,
|
||||
}
|
||||
|
||||
policy = evaluate_policy(session, action=action_id, target=vm_id, source=source)
|
||||
if policy['decision'] == 'blocked':
|
||||
return {
|
||||
'success': False,
|
||||
'status': 'blocked',
|
||||
'result': 'blocked',
|
||||
'decision': 'blocked',
|
||||
'policy_decision': 'blocked',
|
||||
'message': policy['reason'],
|
||||
}
|
||||
if policy['decision'] == 'approval_required':
|
||||
approval = create_approval(
|
||||
session,
|
||||
action=action_id,
|
||||
target=vm_id,
|
||||
source=source,
|
||||
risk=risk,
|
||||
reason=policy['reason'],
|
||||
requester=source,
|
||||
)
|
||||
audit = log_event(
|
||||
session,
|
||||
source=source,
|
||||
identity='operator',
|
||||
action_id=action_id,
|
||||
target=vm_id,
|
||||
target_type='virtual_machine',
|
||||
risk=risk,
|
||||
policy_decision='approval_required',
|
||||
reason=policy['reason'],
|
||||
result='approval_requested',
|
||||
execution_plan={'steps': ['Review VM impact', 'Approve request', 'Execute and verify state']},
|
||||
redacted_metadata={'vmId': vm_id, 'approvalId': approval.id},
|
||||
)
|
||||
return {
|
||||
'success': False,
|
||||
'status': 'approval_required',
|
||||
'result': 'approval_required',
|
||||
'decision': 'approval_required',
|
||||
'policy_decision': 'approval_required',
|
||||
'message': 'VM action requires approval before execution.',
|
||||
'approvalId': approval.id,
|
||||
'auditId': audit.id,
|
||||
}
|
||||
if not confirmed:
|
||||
return {
|
||||
'success': False,
|
||||
'status': 'confirm_required',
|
||||
'result': 'confirm_required',
|
||||
'decision': 'allowed',
|
||||
'policy_decision': 'allowed',
|
||||
'message': 'Explicit confirmation is required before changing VM state.',
|
||||
}
|
||||
return execute_vm_action(
|
||||
session,
|
||||
vm_id=vm_id,
|
||||
action=normalized_action,
|
||||
source=source,
|
||||
policy_decision='allowed',
|
||||
reason=policy['reason'],
|
||||
)
|
||||
|
||||
|
||||
def execute_vm_action(
|
||||
session: Session,
|
||||
*,
|
||||
vm_id: str,
|
||||
action: str,
|
||||
source: str,
|
||||
policy_decision: str,
|
||||
reason: str,
|
||||
) -> dict[str, Any]:
|
||||
definition = VM_ACTIONS.get(action)
|
||||
if not definition or action not in {'start', 'resume', 'pause'}:
|
||||
return {
|
||||
'success': False,
|
||||
'status': 'blocked',
|
||||
'result': 'blocked',
|
||||
'message': 'No safe VM executor is registered for this action.',
|
||||
}
|
||||
|
||||
apply_stored_connector_settings(session)
|
||||
connector = UnraidConnector()
|
||||
before_vm, inventory = _find_vm(connector, vm_id)
|
||||
if not before_vm:
|
||||
message = str(inventory.get('reason') or 'VM target was not found in live Unraid inventory.')
|
||||
audit = log_event(
|
||||
session,
|
||||
source=source,
|
||||
identity='operator',
|
||||
action_id=str(definition['action_id']),
|
||||
target=vm_id,
|
||||
target_type='virtual_machine',
|
||||
risk=str(definition['risk']),
|
||||
policy_decision=policy_decision,
|
||||
reason=reason,
|
||||
result='failed',
|
||||
before_state={'inventoryStatus': inventory.get('status')},
|
||||
redacted_metadata={'vmId': vm_id},
|
||||
)
|
||||
return {
|
||||
'success': False,
|
||||
'status': 'failed',
|
||||
'result': 'failed',
|
||||
'message': message,
|
||||
'auditId': audit.id,
|
||||
}
|
||||
|
||||
before_state = str(before_vm.get('state', 'unknown')).upper()
|
||||
if before_state not in definition['expected_before']:
|
||||
message = f'{definition["label"]} is not valid while VM state is {before_state}.'
|
||||
audit = log_event(
|
||||
session,
|
||||
source=source,
|
||||
identity='operator',
|
||||
action_id=str(definition['action_id']),
|
||||
target=vm_id,
|
||||
target_type='virtual_machine',
|
||||
risk=str(definition['risk']),
|
||||
policy_decision=policy_decision,
|
||||
reason=reason,
|
||||
result='blocked',
|
||||
before_state={'name': before_vm.get('name'), 'state': before_state},
|
||||
redacted_metadata={'vmId': vm_id},
|
||||
)
|
||||
return {
|
||||
'success': False,
|
||||
'status': 'invalid_state',
|
||||
'result': 'blocked',
|
||||
'message': message,
|
||||
'auditId': audit.id,
|
||||
}
|
||||
|
||||
steps = [
|
||||
'Load live VM inventory',
|
||||
f'Validate {before_state} state',
|
||||
f'Run {definition["label"]} through Unraid GraphQL',
|
||||
'Reload VM inventory and verify state',
|
||||
]
|
||||
job = create_job(
|
||||
session,
|
||||
action_id=str(definition['action_id']),
|
||||
target=vm_id,
|
||||
source=source,
|
||||
risk=str(definition['risk']),
|
||||
policy_decision=policy_decision,
|
||||
steps=steps,
|
||||
)
|
||||
start_job(session, job)
|
||||
set_job_progress(session, job, 35, f'Validated VM {before_vm.get("name", vm_id)} in state {before_state}.')
|
||||
|
||||
mutation = connector.mutate_vm(vm_id, action)
|
||||
set_job_progress(session, job, 75, str(mutation.get('message') or 'VM mutation returned.'))
|
||||
after_vm, after_inventory = _find_vm(connector, vm_id)
|
||||
after_state = str(after_vm.get('state', 'unknown')).upper() if after_vm else 'UNKNOWN'
|
||||
verified = after_state in definition['expected_after']
|
||||
success = bool(mutation.get('success')) and verified
|
||||
if success:
|
||||
message = f'{before_vm.get("name", "VM")} is now {after_state.lower()}.'
|
||||
elif mutation.get('success'):
|
||||
message = f'Unraid accepted the action, but post-check reported {after_state}.'
|
||||
else:
|
||||
message = str(mutation.get('message') or 'VM action failed.')
|
||||
|
||||
completed = complete_job(session, job, status='succeeded' if success else 'failed', result_summary=message)
|
||||
audit = log_event(
|
||||
session,
|
||||
source=source,
|
||||
identity='operator',
|
||||
action_id=str(definition['action_id']),
|
||||
target=vm_id,
|
||||
target_type='virtual_machine',
|
||||
risk=str(definition['risk']),
|
||||
policy_decision=policy_decision,
|
||||
reason=reason,
|
||||
result='success' if success else 'failed',
|
||||
execution_plan={'steps': steps, 'jobId': completed.id},
|
||||
before_state={'name': before_vm.get('name'), 'state': before_state},
|
||||
after_state={
|
||||
'name': after_vm.get('name') if after_vm else before_vm.get('name'),
|
||||
'state': after_state,
|
||||
'inventoryStatus': after_inventory.get('status'),
|
||||
'verified': verified,
|
||||
},
|
||||
redacted_metadata={'vmId': vm_id, 'executor': definition['action_id']},
|
||||
)
|
||||
return {
|
||||
'success': success,
|
||||
'status': 'success' if success else 'failed',
|
||||
'result': 'success' if success else 'failed',
|
||||
'decision': policy_decision,
|
||||
'policy_decision': policy_decision,
|
||||
'message': message,
|
||||
'jobId': completed.id,
|
||||
'auditId': audit.id,
|
||||
'before_state': {'name': before_vm.get('name'), 'state': before_state},
|
||||
'after_state': {'state': after_state, 'verified': verified},
|
||||
}
|
||||
|
||||
|
||||
def execute_approved_vm_action(session: Session, approval) -> dict[str, Any]:
|
||||
action = next(
|
||||
(name for name, definition in VM_ACTIONS.items() if definition['action_id'] == approval.action),
|
||||
None,
|
||||
)
|
||||
if action not in {'start', 'resume', 'pause'}:
|
||||
return {
|
||||
'success': False,
|
||||
'status': 'failed',
|
||||
'result': 'unsupported',
|
||||
'message': 'No safe approved VM executor is registered for this action.',
|
||||
}
|
||||
return execute_vm_action(
|
||||
session,
|
||||
vm_id=approval.target,
|
||||
action=action,
|
||||
source='approved_operator',
|
||||
policy_decision='approval_required',
|
||||
reason=approval.reason or f'Approved through {approval.id}.',
|
||||
)
|
||||
Reference in new issue
Block a user