rasa2 commited on
Commit
de12842
·
1 Parent(s): 77b29cf

Add minimal Fury broker app

Browse files
Files changed (3) hide show
  1. Dockerfile +10 -0
  2. app.py +175 -0
  3. requirements.txt +4 -0
Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY app.py .
9
+
10
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+ from enum import Enum
4
+ from typing import Optional
5
+
6
+ from fastapi import FastAPI, Header, HTTPException, Form
7
+ from fastapi.responses import HTMLResponse, RedirectResponse
8
+ from pydantic import BaseModel
9
+
10
+
11
+ BROKER_TOKEN = os.environ.get("BROKER_TOKEN")
12
+
13
+ if not BROKER_TOKEN:
14
+ raise RuntimeError("Missing BROKER_TOKEN environment variable")
15
+
16
+
17
+ class JobStatus(str, Enum):
18
+ queued = "queued"
19
+ running = "running"
20
+ done = "done"
21
+ failed = "failed"
22
+
23
+
24
+ class Job(BaseModel):
25
+ id: str
26
+ command: str
27
+ status: JobStatus = JobStatus.queued
28
+ result: Optional[str] = None
29
+
30
+
31
+ app = FastAPI()
32
+
33
+ jobs: dict[str, Job] = {}
34
+
35
+
36
+ ALLOWED_COMMANDS = {
37
+ "hostname": "hostname",
38
+ "whoami": "whoami",
39
+ "pwd": "pwd",
40
+ "disk": "df -h",
41
+ "date": "date",
42
+ }
43
+
44
+
45
+ def verify_token(authorization: Optional[str]) -> None:
46
+ expected = f"Bearer {BROKER_TOKEN}"
47
+ if authorization != expected:
48
+ raise HTTPException(status_code=401, detail="Unauthorized")
49
+
50
+
51
+ @app.get("/", response_class=HTMLResponse)
52
+ def home():
53
+ rows = ""
54
+
55
+ for job in reversed(list(jobs.values())):
56
+ result = job.result or ""
57
+ rows += f"""
58
+ <tr>
59
+ <td>{job.id}</td>
60
+ <td>{job.command}</td>
61
+ <td>{job.status}</td>
62
+ <td><pre>{result}</pre></td>
63
+ </tr>
64
+ """
65
+
66
+ options = "\n".join(
67
+ f'<option value="{name}">{name} -> {cmd}</option>'
68
+ for name, cmd in ALLOWED_COMMANDS.items()
69
+ )
70
+
71
+ return f"""
72
+ <html>
73
+ <head>
74
+ <title>Fury Broker</title>
75
+ <style>
76
+ body {{ font-family: sans-serif; margin: 40px; }}
77
+ table {{ border-collapse: collapse; width: 100%; }}
78
+ th, td {{ border: 1px solid #ccc; padding: 8px; vertical-align: top; }}
79
+ pre {{ white-space: pre-wrap; max-width: 700px; }}
80
+ </style>
81
+ </head>
82
+ <body>
83
+ <h1>Fury Broker</h1>
84
+
85
+ <form method="post" action="/submit">
86
+ <label>Command:</label>
87
+ <select name="command">
88
+ {options}
89
+ </select>
90
+ <button type="submit">Submit job</button>
91
+ </form>
92
+
93
+ <h2>Jobs</h2>
94
+ <table>
95
+ <tr>
96
+ <th>ID</th>
97
+ <th>Command</th>
98
+ <th>Status</th>
99
+ <th>Result</th>
100
+ </tr>
101
+ {rows}
102
+ </table>
103
+ </body>
104
+ </html>
105
+ """
106
+
107
+
108
+ @app.post("/submit")
109
+ def submit_from_ui(command: str = Form(...)):
110
+ if command not in ALLOWED_COMMANDS:
111
+ raise HTTPException(status_code=400, detail="Command is not allowed")
112
+
113
+ job_id = str(uuid.uuid4())
114
+ jobs[job_id] = Job(
115
+ id=job_id,
116
+ command=command,
117
+ status=JobStatus.queued,
118
+ )
119
+
120
+ return RedirectResponse("/", status_code=303)
121
+
122
+
123
+ @app.post("/api/jobs")
124
+ def submit_job(
125
+ command: str,
126
+ authorization: Optional[str] = Header(default=None),
127
+ ):
128
+ verify_token(authorization)
129
+
130
+ if command not in ALLOWED_COMMANDS:
131
+ raise HTTPException(status_code=400, detail="Command is not allowed")
132
+
133
+ job_id = str(uuid.uuid4())
134
+ job = Job(
135
+ id=job_id,
136
+ command=command,
137
+ status=JobStatus.queued,
138
+ )
139
+ jobs[job_id] = job
140
+ return job
141
+
142
+
143
+ @app.get("/api/next-job")
144
+ def get_next_job(authorization: Optional[str] = Header(default=None)):
145
+ verify_token(authorization)
146
+
147
+ for job in jobs.values():
148
+ if job.status == JobStatus.queued:
149
+ job.status = JobStatus.running
150
+ return {
151
+ "id": job.id,
152
+ "command": job.command,
153
+ "shell_command": ALLOWED_COMMANDS[job.command],
154
+ }
155
+
156
+ return {"id": None}
157
+
158
+
159
+ @app.post("/api/jobs/{job_id}/result")
160
+ def post_result(
161
+ job_id: str,
162
+ result: str,
163
+ success: bool,
164
+ authorization: Optional[str] = Header(default=None),
165
+ ):
166
+ verify_token(authorization)
167
+
168
+ if job_id not in jobs:
169
+ raise HTTPException(status_code=404, detail="Job not found")
170
+
171
+ job = jobs[job_id]
172
+ job.result = result
173
+ job.status = JobStatus.done if success else JobStatus.failed
174
+
175
+ return job
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-multipart
4
+ jinja2