Jobs interface is looking really good now..

This commit is contained in:
Michael Woods
2025-12-28 00:09:47 -05:00
parent e54ba05c19
commit e7d308ab69
3 changed files with 58 additions and 4 deletions

View File

@@ -7,6 +7,7 @@ import logging
import base64 import base64
import json import json
import gzip import gzip
import shlex
from traceback import format_exc from traceback import format_exc
from packetserver.http.dependencies import get_current_http_user from packetserver.http.dependencies import get_current_http_user
@@ -20,6 +21,16 @@ from packetserver.server.jobs import RunnerFile
router = APIRouter(prefix="/api/v1", tags=["jobs"]) router = APIRouter(prefix="/api/v1", tags=["jobs"])
dashboard_router = APIRouter(tags=["jobs"]) dashboard_router = APIRouter(tags=["jobs"])
def tokenize_cmd(cmd: str) -> list[str]:
"""
Tokenize a command string with basic shell-like quoting support.
Uses shlex with posix=True for proper "double" and 'single' quote handling.
"""
try:
return shlex.split(cmd)
except ValueError as e:
raise HTTPException(status_code=400, detail=f"Invalid command quoting: {e}")
class JobSummary(BaseModel): class JobSummary(BaseModel):
id: int id: int
cmd: Union[str, List[str]] cmd: Union[str, List[str]]
@@ -161,6 +172,7 @@ async def create_job_from_form(
env_values: List[str] = Form(default=[]), env_values: List[str] = Form(default=[]),
files: List[UploadFile] = File(default=[]), files: List[UploadFile] = File(default=[]),
include_db: Optional[str] = Form(None), include_db: Optional[str] = Form(None),
shell_mode: Optional[str] = Form(None),
current_user: HttpUser = Depends(get_current_http_user) current_user: HttpUser = Depends(get_current_http_user)
): ):
# Build env dict from parallel lists # Build env dict from parallel lists
@@ -176,6 +188,15 @@ async def create_job_from_form(
content = await upload.read() content = await upload.read()
files_dict[upload.filename] = base64.b64encode(content).decode('ascii') files_dict[upload.filename] = base64.b64encode(content).decode('ascii')
if shell_mode == "on":
# Pass entire raw cmd string to bash -c
cmd_args = ["bash", "-c", cmd.strip()]
else:
# Use shlex to respect quotes
cmd_args = tokenize_cmd(cmd.strip())
if not cmd_args:
raise HTTPException(status_code=400, detail="Command cannot be empty")
if include_db == "on": if include_db == "on":
try: try:
username_lower = current_user.username.lower() username_lower = current_user.username.lower()
@@ -191,14 +212,13 @@ async def create_job_from_form(
# Prepare payload for the existing API # Prepare payload for the existing API
payload = { payload = {
"cmd": [part.strip() for part in cmd.split() if part.strip()], # split on whitespace, like shell "cmd": cmd_args,
"env": env if env else None, "env": env if env else None,
"files": files_dict if files_dict else None "files": files_dict if files_dict else None
} }
# Call the API internally # Call the API internally
from packetserver.http.routers.jobs import create_job as api_create_job response = await create_job(
response = await api_create_job(
payload=JobCreate(**{k: v for k, v in payload.items() if v is not None}), payload=JobCreate(**{k: v for k, v in payload.items() if v is not None}),
db=db, db=db,
current_user=current_user current_user=current_user

View File

@@ -10,9 +10,33 @@
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">
Command Command
{% if job.cmd|length > 1 or ('bash' in job.cmd and job.cmd|length == 3) %}
<span class="badge bg-info float-end">Multi-arg</span>
{% elif job.cmd|length == 1 %}
<span class="badge bg-secondary float-end">Single arg</span>
{% endif %}
</div> </div>
<div class="card-body"> <div class="card-body">
<pre><code>{% if job.cmd is string %}{{ job.cmd }}{% else %}{{ job.cmd|join(' ') }}{% endif %}</code></pre> {% if job.cmd is string %}
<!-- Legacy fallback: old jobs stored cmd as single string -->
<pre><code>{{ job.cmd }}</code></pre>
<small class="text-muted">Legacy single-string command</small>
{% else %}
<!-- Modern list display -->
<ol class="mb-0 ps-4">
{% for arg in job.cmd %}
<li class="mb-2">
<code>{{ arg | e }}</code>
</li>
{% endfor %}
</ol>
{% if job.cmd[:2] == ['bash', '-c'] %}
<hr class="my-3">
<p class="mb-0"><strong>Full command passed to bash -c:</strong></p>
<pre><code>{{ job.cmd[2] }}</code></pre>
<small class="text-muted">This job used shell mode</small>
{% endif %}
{% endif %}
</div> </div>
</div> </div>

View File

@@ -13,6 +13,16 @@
<textarea name="cmd" class="form-control" rows="3" placeholder="e.g. python script.py --input data.txt" required></textarea> <textarea name="cmd" class="form-control" rows="3" placeholder="e.g. python script.py --input data.txt" required></textarea>
<div class="form-text">Space-separated command and arguments (like a shell command).</div> <div class="form-text">Space-separated command and arguments (like a shell command).</div>
</div> </div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="shell_mode" name="shell_mode" value="on">
<label class="form-check-label" for="shell_mode">
Run command through shell (bash -c)
<small class="text-muted d-block">
Allows full shell syntax, pipes, redirects, globbing, and quoted arguments with spaces.
The entire command will be passed as a single string to <code>bash -c</code>.
</small>
</label>
</div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Environment Variables</label> <label class="form-label">Environment Variables</label>