Jobs interface is looking really good now..
This commit is contained in:
@@ -7,6 +7,7 @@ import logging
|
||||
import base64
|
||||
import json
|
||||
import gzip
|
||||
import shlex
|
||||
from traceback import format_exc
|
||||
|
||||
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"])
|
||||
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):
|
||||
id: int
|
||||
cmd: Union[str, List[str]]
|
||||
@@ -161,6 +172,7 @@ async def create_job_from_form(
|
||||
env_values: List[str] = Form(default=[]),
|
||||
files: List[UploadFile] = File(default=[]),
|
||||
include_db: Optional[str] = Form(None),
|
||||
shell_mode: Optional[str] = Form(None),
|
||||
current_user: HttpUser = Depends(get_current_http_user)
|
||||
):
|
||||
# Build env dict from parallel lists
|
||||
@@ -176,6 +188,15 @@ async def create_job_from_form(
|
||||
content = await upload.read()
|
||||
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":
|
||||
try:
|
||||
username_lower = current_user.username.lower()
|
||||
@@ -191,14 +212,13 @@ async def create_job_from_form(
|
||||
|
||||
# Prepare payload for the existing API
|
||||
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,
|
||||
"files": files_dict if files_dict else None
|
||||
}
|
||||
|
||||
# Call the API internally
|
||||
from packetserver.http.routers.jobs import create_job as api_create_job
|
||||
response = await api_create_job(
|
||||
response = await create_job(
|
||||
payload=JobCreate(**{k: v for k, v in payload.items() if v is not None}),
|
||||
db=db,
|
||||
current_user=current_user
|
||||
|
||||
@@ -10,9 +10,33 @@
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
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 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>
|
||||
|
||||
|
||||
@@ -13,6 +13,16 @@
|
||||
<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>
|
||||
<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">
|
||||
<label class="form-label">Environment Variables</label>
|
||||
|
||||
Reference in New Issue
Block a user