Files
aitbc/cli/commands/admin.py
aitbc1 c0952c2525 refactor: flatten CLI directory structure - remove 'box in a box'
BEFORE:
/opt/aitbc/cli/
├── aitbc_cli/                    # Python package (box in a box)
│   ├── commands/
│   ├── main.py
│   └── ...
├── setup.py

AFTER:
/opt/aitbc/cli/                    # Flat structure
├── commands/                      # Direct access
├── main.py                        # Direct access
├── auth/
├── config/
├── core/
├── models/
├── utils/
├── plugins.py
└── setup.py

CHANGES MADE:
- Moved all files from aitbc_cli/ to cli/ root
- Fixed all relative imports (from . to absolute imports)
- Updated setup.py entry point: aitbc_cli.main → main
- Added CLI directory to Python path in entry script
- Simplified deployment.py to remove dependency on deleted core.deployment
- Fixed import paths in all command files
- Recreated virtual environment with new structure

BENEFITS:
- Eliminated 'box in a box' nesting
- Simpler directory structure
- Direct access to all modules
- Cleaner imports
- Easier maintenance and development
- CLI works with both 'python main.py' and 'aitbc' commands
2026-03-26 09:12:02 +01:00

513 lines
16 KiB
Python
Executable File

"""Admin commands for AITBC CLI"""
import click
import httpx
import json
from typing import Optional, List, Dict, Any
from utils import output, error, success
@click.group()
@click.pass_context
def admin(ctx):
"""System administration commands"""
# Set role for admin commands
ctx.ensure_object(dict)
ctx.parent.detected_role = 'admin'
@admin.command()
@click.pass_context
def status(ctx):
"""Show system status"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url.rstrip('/')}/v1/admin/stats",
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
status_data = response.json()
output(status_data, ctx.obj['output_format'])
else:
error(f"Failed to get status: {response.status_code}")
ctx.exit(1)
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@admin.command()
@click.option("--output", type=click.Path(), help="Output report to file")
@click.pass_context
def audit_verify(ctx, output):
"""Verify audit log integrity"""
audit_logger = AuditLogger()
is_valid, issues = audit_logger.verify_integrity()
if is_valid:
success("Audit log integrity verified - no tampering detected")
else:
error("Audit log integrity compromised!")
for issue in issues:
error(f" - {issue}")
ctx.exit(1)
# Export detailed report if requested
if output:
try:
report = audit_logger.export_report(Path(output))
success(f"Audit report exported to {output}")
# Show summary
stats = report["audit_report"]["statistics"]
output({
"total_entries": stats["total_entries"],
"unique_actions": stats["unique_actions"],
"unique_users": stats["unique_users"],
"date_range": stats["date_range"]
}, ctx.obj['output_format'])
except Exception as e:
error(f"Failed to export report: {e}")
@admin.command()
@click.option("--limit", default=50, help="Number of entries to show")
@click.option("--action", help="Filter by action type")
@click.option("--search", help="Search query")
@click.pass_context
def audit_logs(ctx, limit: int, action: str, search: str):
"""View audit logs with integrity verification"""
audit_logger = AuditLogger()
try:
if search:
entries = audit_logger.search_logs(search, limit)
else:
entries = audit_logger.get_logs(limit, action)
if not entries:
warning("No audit entries found")
return
# Show entries
output({
"total_entries": len(entries),
"entries": entries
}, ctx.obj['output_format'])
except Exception as e:
error(f"Failed to read audit logs: {e}")
ctx.exit(1)
@admin.command()
@click.option("--limit", default=50, help="Number of jobs to show")
@click.option("--status", help="Filter by status")
@click.pass_context
def jobs(ctx, limit: int, status: Optional[str]):
"""List all jobs in the system"""
config = ctx.obj['config']
try:
params = {"limit": limit}
if status:
params["status"] = status
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/admin/jobs",
params=params,
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
jobs = response.json()
output(jobs, ctx.obj['output_format'])
else:
error(f"Failed to get jobs: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@admin.command()
@click.argument("job_id")
@click.pass_context
def job_details(ctx, job_id: str):
"""Get detailed job information"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/admin/jobs/{job_id}",
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
job_data = response.json()
output(job_data, ctx.obj['output_format'])
else:
error(f"Job not found: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@admin.command()
@click.argument("job_id")
@click.pass_context
def delete_job(ctx, job_id: str):
"""Delete a job from the system"""
config = ctx.obj['config']
if not click.confirm(f"Are you sure you want to delete job {job_id}?"):
return
try:
with httpx.Client() as client:
response = client.delete(
f"{config.coordinator_url}/admin/jobs/{job_id}",
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
success(f"Job {job_id} deleted")
output({"status": "deleted", "job_id": job_id}, ctx.obj['output_format'])
else:
error(f"Failed to delete job: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@admin.command()
@click.option("--limit", default=50, help="Number of miners to show")
@click.option("--status", help="Filter by status")
@click.pass_context
def miners(ctx, limit: int, status: Optional[str]):
"""List all registered miners"""
config = ctx.obj['config']
try:
params = {"limit": limit}
if status:
params["status"] = status
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/admin/miners",
params=params,
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
miners = response.json()
output(miners, ctx.obj['output_format'])
else:
error(f"Failed to get miners: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@admin.command()
@click.argument("miner_id")
@click.pass_context
def miner_details(ctx, miner_id: str):
"""Get detailed miner information"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/admin/miners/{miner_id}",
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
miner_data = response.json()
output(miner_data, ctx.obj['output_format'])
else:
error(f"Miner not found: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@admin.command()
@click.argument("miner_id")
@click.pass_context
def deactivate_miner(ctx, miner_id: str):
"""Deactivate a miner"""
config = ctx.obj['config']
if not click.confirm(f"Are you sure you want to deactivate miner {miner_id}?"):
return
try:
with httpx.Client() as client:
response = client.post(
f"{config.coordinator_url}/admin/miners/{miner_id}/deactivate",
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
success(f"Miner {miner_id} deactivated")
output({"status": "deactivated", "miner_id": miner_id}, ctx.obj['output_format'])
else:
error(f"Failed to deactivate miner: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@admin.command()
@click.argument("miner_id")
@click.pass_context
def activate_miner(ctx, miner_id: str):
"""Activate a miner"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.post(
f"{config.coordinator_url}/admin/miners/{miner_id}/activate",
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
success(f"Miner {miner_id} activated")
output({"status": "activated", "miner_id": miner_id}, ctx.obj['output_format'])
else:
error(f"Failed to activate miner: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@admin.command()
@click.option("--days", type=int, default=7, help="Number of days to analyze")
@click.pass_context
def analytics(ctx, days: int):
"""Get system analytics"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/admin/analytics",
params={"days": days},
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
analytics_data = response.json()
output(analytics_data, ctx.obj['output_format'])
else:
error(f"Failed to get analytics: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@admin.command()
@click.option("--level", default="INFO", help="Log level (DEBUG, INFO, WARNING, ERROR)")
@click.option("--limit", default=100, help="Number of log entries to show")
@click.pass_context
def logs(ctx, level: str, limit: int):
"""Get system logs"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.get(
f"{config.coordinator_url}/admin/logs",
params={"level": level, "limit": limit},
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
logs_data = response.json()
output(logs_data, ctx.obj['output_format'])
else:
error(f"Failed to get logs: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@admin.command()
@click.argument("job_id")
@click.option("--reason", help="Reason for priority change")
@click.pass_context
def prioritize_job(ctx, job_id: str, reason: Optional[str]):
"""Set job to high priority"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.post(
f"{config.coordinator_url}/admin/jobs/{job_id}/prioritize",
json={"reason": reason or "Admin priority"},
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
success(f"Job {job_id} prioritized")
output({"status": "prioritized", "job_id": job_id}, ctx.obj['output_format'])
else:
error(f"Failed to prioritize job: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@admin.command()
@click.option("--action", required=True, help="Action to perform")
@click.option("--target", help="Target of the action")
@click.option("--data", help="Additional data (JSON)")
@click.pass_context
def execute(ctx, action: str, target: Optional[str], data: Optional[str]):
"""Execute custom admin action"""
config = ctx.obj['config']
# Parse data if provided
parsed_data = {}
if data:
try:
parsed_data = json.loads(data)
except json.JSONDecodeError:
error("Invalid JSON data")
return
if target:
parsed_data["target"] = target
try:
with httpx.Client() as client:
response = client.post(
f"{config.coordinator_url}/admin/execute/{action}",
json=parsed_data,
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
result = response.json()
output(result, ctx.obj['output_format'])
else:
error(f"Failed to execute action: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@admin.group()
def maintenance():
"""Maintenance operations"""
pass
@maintenance.command()
@click.pass_context
def cleanup(ctx):
"""Clean up old jobs and data"""
config = ctx.obj['config']
if not click.confirm("This will clean up old jobs and temporary data. Continue?"):
return
try:
with httpx.Client() as client:
response = client.post(
f"{config.coordinator_url}/admin/maintenance/cleanup",
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
result = response.json()
success("Cleanup completed")
output(result, ctx.obj['output_format'])
else:
error(f"Cleanup failed: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@maintenance.command()
@click.pass_context
def reindex(ctx):
"""Reindex the database"""
config = ctx.obj['config']
if not click.confirm("This will reindex the entire database. Continue?"):
return
try:
with httpx.Client() as client:
response = client.post(
f"{config.coordinator_url}/admin/maintenance/reindex",
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
result = response.json()
success("Reindex started")
output(result, ctx.obj['output_format'])
else:
error(f"Reindex failed: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@maintenance.command()
@click.pass_context
def backup(ctx):
"""Create system backup"""
config = ctx.obj['config']
try:
with httpx.Client() as client:
response = client.post(
f"{config.coordinator_url}/admin/maintenance/backup",
headers={"X-Api-Key": config.api_key or ""}
)
if response.status_code == 200:
result = response.json()
success("Backup created")
output(result, ctx.obj['output_format'])
else:
error(f"Backup failed: {response.status_code}")
except Exception as e:
error(f"Network error: {e}")
ctx.exit(1)
@admin.command(name="audit-log")
@click.option("--limit", default=50, help="Number of entries to show")
@click.option("--action", "action_filter", help="Filter by action type")
@click.pass_context
def audit_log(ctx, limit: int, action_filter: Optional[str]):
"""View audit log"""
from utils import AuditLogger
logger = AuditLogger()
entries = logger.get_logs(limit=limit, action_filter=action_filter)
if not entries:
output({"message": "No audit log entries found"}, ctx.obj['output_format'])
return
output(entries, ctx.obj['output_format'])
# Add maintenance group to admin
admin.add_command(maintenance)