Allow selecting svcBorealis account for playbooks

This commit is contained in:
2025-10-15 02:58:55 -06:00
parent 74540b7f10
commit 2f8ff949fc
4 changed files with 154 additions and 41 deletions

View File

@@ -429,6 +429,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
const [credentialLoading, setCredentialLoading] = useState(false); const [credentialLoading, setCredentialLoading] = useState(false);
const [credentialError, setCredentialError] = useState(""); const [credentialError, setCredentialError] = useState("");
const [selectedCredentialId, setSelectedCredentialId] = useState(""); const [selectedCredentialId, setSelectedCredentialId] = useState("");
const [useSvcAccount, setUseSvcAccount] = useState(true);
const loadCredentials = useCallback(async () => { const loadCredentials = useCallback(async () => {
setCredentialLoading(true); setCredentialLoading(true);
@@ -453,6 +454,16 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
}, [loadCredentials]); }, [loadCredentials]);
const remoteExec = useMemo(() => execContext === "ssh" || execContext === "winrm", [execContext]); const remoteExec = useMemo(() => execContext === "ssh" || execContext === "winrm", [execContext]);
const handleExecContextChange = useCallback((value) => {
const normalized = String(value || "system").toLowerCase();
setExecContext(normalized);
if (normalized === "winrm") {
setUseSvcAccount(true);
setSelectedCredentialId("");
} else {
setUseSvcAccount(false);
}
}, []);
const filteredCredentials = useMemo(() => { const filteredCredentials = useMemo(() => {
if (!remoteExec) return credentials; if (!remoteExec) return credentials;
const target = execContext === "winrm" ? "winrm" : "ssh"; const target = execContext === "winrm" ? "winrm" : "ssh";
@@ -463,6 +474,10 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
if (!remoteExec) { if (!remoteExec) {
return; return;
} }
if (execContext === "winrm" && useSvcAccount) {
setSelectedCredentialId("");
return;
}
if (!filteredCredentials.length) { if (!filteredCredentials.length) {
setSelectedCredentialId(""); setSelectedCredentialId("");
return; return;
@@ -470,7 +485,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
if (!selectedCredentialId || !filteredCredentials.some((cred) => String(cred.id) === String(selectedCredentialId))) { if (!selectedCredentialId || !filteredCredentials.some((cred) => String(cred.id) === String(selectedCredentialId))) {
setSelectedCredentialId(String(filteredCredentials[0].id)); setSelectedCredentialId(String(filteredCredentials[0].id));
} }
}, [remoteExec, filteredCredentials, selectedCredentialId]); }, [remoteExec, filteredCredentials, selectedCredentialId, execContext, useSvcAccount]);
// dialogs state // dialogs state
const [addCompOpen, setAddCompOpen] = useState(false); const [addCompOpen, setAddCompOpen] = useState(false);
@@ -877,12 +892,13 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
const isValid = useMemo(() => { const isValid = useMemo(() => {
const base = jobName.trim().length > 0 && components.length > 0 && targets.length > 0; const base = jobName.trim().length > 0 && components.length > 0 && targets.length > 0;
if (!base) return false; if (!base) return false;
if (remoteExec && !selectedCredentialId) return false; const needsCredential = remoteExec && !(execContext === "winrm" && useSvcAccount);
if (needsCredential && !selectedCredentialId) return false;
if (scheduleType !== "immediately") { if (scheduleType !== "immediately") {
return !!startDateTime; return !!startDateTime;
} }
return true; return true;
}, [jobName, components.length, targets.length, scheduleType, startDateTime, remoteExec, selectedCredentialId]); }, [jobName, components.length, targets.length, scheduleType, startDateTime, remoteExec, selectedCredentialId, execContext, useSvcAccount]);
const [confirmOpen, setConfirmOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false);
const editing = !!(initialJob && initialJob.id); const editing = !!(initialJob && initialJob.id);
@@ -1358,6 +1374,11 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
setExpiration(initialJob.expiration || "no_expire"); setExpiration(initialJob.expiration || "no_expire");
setExecContext(initialJob.execution_context || "system"); setExecContext(initialJob.execution_context || "system");
setSelectedCredentialId(initialJob.credential_id ? String(initialJob.credential_id) : ""); setSelectedCredentialId(initialJob.credential_id ? String(initialJob.credential_id) : "");
if ((initialJob.execution_context || "").toLowerCase() === "winrm") {
setUseSvcAccount(initialJob.use_service_account !== false);
} else {
setUseSvcAccount(false);
}
const comps = Array.isArray(initialJob.components) ? initialJob.components : []; const comps = Array.isArray(initialJob.components) ? initialJob.components : [];
const hydrated = await hydrateExistingComponents(comps); const hydrated = await hydrateExistingComponents(comps);
if (!canceled) { if (!canceled) {
@@ -1369,6 +1390,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
setComponents([]); setComponents([]);
setComponentVarErrors({}); setComponentVarErrors({});
setSelectedCredentialId(""); setSelectedCredentialId("");
setUseSvcAccount(true);
} }
}; };
hydrate(); hydrate();
@@ -1464,7 +1486,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
}; };
const handleCreate = async () => { const handleCreate = async () => {
if (remoteExec && !selectedCredentialId) { if (remoteExec && !(execContext === "winrm" && useSvcAccount) && !selectedCredentialId) {
alert("Please select a credential for this execution context."); alert("Please select a credential for this execution context.");
return; return;
} }
@@ -1496,7 +1518,8 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
schedule: { type: scheduleType, start: scheduleType !== "immediately" ? (() => { try { const d = startDateTime?.toDate?.() || new Date(startDateTime); d.setSeconds(0,0); return d.toISOString(); } catch { return startDateTime; } })() : null }, schedule: { type: scheduleType, start: scheduleType !== "immediately" ? (() => { try { const d = startDateTime?.toDate?.() || new Date(startDateTime); d.setSeconds(0,0); return d.toISOString(); } catch { return startDateTime; } })() : null },
duration: { stopAfterEnabled, expiration }, duration: { stopAfterEnabled, expiration },
execution_context: execContext, execution_context: execContext,
credential_id: remoteExec && selectedCredentialId ? Number(selectedCredentialId) : null credential_id: remoteExec && !useSvcAccount && selectedCredentialId ? Number(selectedCredentialId) : null,
use_service_account: execContext === "winrm" ? Boolean(useSvcAccount) : false
}; };
try { try {
const resp = await fetch(initialJob && initialJob.id ? `/api/scheduled_jobs/${initialJob.id}` : "/api/scheduled_jobs", { const resp = await fetch(initialJob && initialJob.id ? `/api/scheduled_jobs/${initialJob.id}` : "/api/scheduled_jobs", {
@@ -1726,7 +1749,7 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
<Select <Select
size="small" size="small"
value={execContext} value={execContext}
onChange={(e) => setExecContext(e.target.value)} onChange={(e) => handleExecContextChange(e.target.value)}
sx={{ minWidth: 320 }} sx={{ minWidth: 320 }}
> >
<MenuItem value="system">Run on agent as SYSTEM (device-local)</MenuItem> <MenuItem value="system">Run on agent as SYSTEM (device-local)</MenuItem>
@@ -1736,10 +1759,29 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
</Select> </Select>
{remoteExec && ( {remoteExec && (
<Box sx={{ mt: 2, display: "flex", alignItems: "center", gap: 1.5, flexWrap: "wrap" }}> <Box sx={{ mt: 2, display: "flex", alignItems: "center", gap: 1.5, flexWrap: "wrap" }}>
{execContext === "winrm" && (
<FormControlLabel
control={
<Checkbox
checked={useSvcAccount}
onChange={(e) => {
const checked = e.target.checked;
setUseSvcAccount(checked);
if (checked) {
setSelectedCredentialId("");
} else if (!selectedCredentialId && filteredCredentials.length) {
setSelectedCredentialId(String(filteredCredentials[0].id));
}
}}
/>
}
label="Use Configured svcBorealis Account"
/>
)}
<FormControl <FormControl
size="small" size="small"
sx={{ minWidth: 320 }} sx={{ minWidth: 320 }}
disabled={credentialLoading || !filteredCredentials.length} disabled={credentialLoading || !filteredCredentials.length || (execContext === "winrm" && useSvcAccount)}
> >
<InputLabel sx={{ color: "#aaa" }}>Credential</InputLabel> <InputLabel sx={{ color: "#aaa" }}>Credential</InputLabel>
<Select <Select
@@ -1771,7 +1813,12 @@ export default function CreateJob({ onCancel, onCreated, initialJob = null }) {
{credentialError} {credentialError}
</Typography> </Typography>
)} )}
{!credentialLoading && !credentialError && !filteredCredentials.length && ( {execContext === "winrm" && useSvcAccount && (
<Typography variant="body2" sx={{ color: "#aaa" }}>
Runs with the agent&apos;s svcBorealis account.
</Typography>
)}
{!credentialLoading && !credentialError && !filteredCredentials.length && (!(execContext === "winrm" && useSvcAccount)) && (
<Typography variant="body2" sx={{ color: "#ff8080" }}> <Typography variant="body2" sx={{ color: "#ff8080" }}>
No {execContext === "winrm" ? "WinRM" : "SSH"} credentials available. Create one under Access Management &gt; Credentials. No {execContext === "winrm" ? "WinRM" : "SSH"} credentials available. Create one under Access Management &gt; Credentials.
</Typography> </Typography>

View File

@@ -91,6 +91,7 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
const [credentialsLoading, setCredentialsLoading] = useState(false); const [credentialsLoading, setCredentialsLoading] = useState(false);
const [credentialsError, setCredentialsError] = useState(""); const [credentialsError, setCredentialsError] = useState("");
const [selectedCredentialId, setSelectedCredentialId] = useState(""); const [selectedCredentialId, setSelectedCredentialId] = useState("");
const [useSvcAccount, setUseSvcAccount] = useState(true);
const [variables, setVariables] = useState([]); const [variables, setVariables] = useState([]);
const [variableValues, setVariableValues] = useState({}); const [variableValues, setVariableValues] = useState({});
const [variableErrors, setVariableErrors] = useState({}); const [variableErrors, setVariableErrors] = useState({});
@@ -120,6 +121,8 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
setVariableValues({}); setVariableValues({});
setVariableErrors({}); setVariableErrors({});
setVariableStatus({ loading: false, error: "" }); setVariableStatus({ loading: false, error: "" });
setUseSvcAccount(true);
setSelectedCredentialId("");
loadTree(); loadTree();
} }
}, [open, loadTree]); }, [open, loadTree]);
@@ -164,7 +167,7 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
}, [open]); }, [open]);
useEffect(() => { useEffect(() => {
if (mode !== "ansible") return; if (mode !== "ansible" || useSvcAccount) return;
if (!credentials.length) { if (!credentials.length) {
setSelectedCredentialId(""); setSelectedCredentialId("");
return; return;
@@ -172,7 +175,7 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
if (!selectedCredentialId || !credentials.some((cred) => String(cred.id) === String(selectedCredentialId))) { if (!selectedCredentialId || !credentials.some((cred) => String(cred.id) === String(selectedCredentialId))) {
setSelectedCredentialId(String(credentials[0].id)); setSelectedCredentialId(String(credentials[0].id));
} }
}, [mode, credentials, selectedCredentialId]); }, [mode, credentials, selectedCredentialId, useSvcAccount]);
const renderNodes = (nodes = []) => const renderNodes = (nodes = []) =>
nodes.map((n) => ( nodes.map((n) => (
@@ -345,7 +348,7 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
setError(mode === 'ansible' ? "Please choose a playbook to run." : "Please choose a script to run."); setError(mode === 'ansible' ? "Please choose a playbook to run." : "Please choose a script to run.");
return; return;
} }
if (mode === 'ansible' && !selectedCredentialId) { if (mode === 'ansible' && !useSvcAccount && !selectedCredentialId) {
setError("Select a credential to run this playbook."); setError("Select a credential to run this playbook.");
return; return;
} }
@@ -381,7 +384,8 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
playbook_path, playbook_path,
hostnames, hostnames,
variable_values: variableOverrides, variable_values: variableOverrides,
credential_id: selectedCredentialId ? Number(selectedCredentialId) : null credential_id: !useSvcAccount && selectedCredentialId ? Number(selectedCredentialId) : null,
use_service_account: Boolean(useSvcAccount)
}) })
}); });
} else { } else {
@@ -408,8 +412,11 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
} }
}; };
const credentialRequired = mode === "ansible"; const credentialRequired = mode === "ansible" && !useSvcAccount;
const disableRun = running || !selectedPath || (credentialRequired && (!selectedCredentialId || !credentials.length)); const disableRun =
running ||
!selectedPath ||
(credentialRequired && (!selectedCredentialId || !credentials.length));
return ( return (
<Dialog open={open} onClose={running ? undefined : onClose} fullWidth maxWidth="md" <Dialog open={open} onClose={running ? undefined : onClose} fullWidth maxWidth="md"
@@ -426,10 +433,29 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
</Typography> </Typography>
{mode === 'ansible' && ( {mode === 'ansible' && (
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5, flexWrap: "wrap", mb: 2 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1.5, flexWrap: "wrap", mb: 2 }}>
<FormControlLabel
control={
<Checkbox
checked={useSvcAccount}
onChange={(e) => {
const checked = e.target.checked;
setUseSvcAccount(checked);
if (checked) {
setSelectedCredentialId("");
} else if (!selectedCredentialId && credentials.length) {
setSelectedCredentialId(String(credentials[0].id));
}
}}
size="small"
/>
}
label="Use Configured svcBorealis Account"
sx={{ mr: 2 }}
/>
<FormControl <FormControl
size="small" size="small"
sx={{ minWidth: 260 }} sx={{ minWidth: 260 }}
disabled={credentialsLoading || !credentials.length} disabled={useSvcAccount || credentialsLoading || !credentials.length}
> >
<InputLabel sx={{ color: "#aaa" }}>Credential</InputLabel> <InputLabel sx={{ color: "#aaa" }}>Credential</InputLabel>
<Select <Select
@@ -449,11 +475,16 @@ export default function QuickJob({ open, onClose, hostnames = [] }) {
})} })}
</Select> </Select>
</FormControl> </FormControl>
{useSvcAccount && (
<Typography variant="body2" sx={{ color: "#aaa" }}>
Runs with the agent&apos;s svcBorealis account.
</Typography>
)}
{credentialsLoading && <CircularProgress size={18} sx={{ color: "#58a6ff" }} />} {credentialsLoading && <CircularProgress size={18} sx={{ color: "#58a6ff" }} />}
{!credentialsLoading && credentialsError && ( {!credentialsLoading && credentialsError && (
<Typography variant="body2" sx={{ color: "#ff8080" }}>{credentialsError}</Typography> <Typography variant="body2" sx={{ color: "#ff8080" }}>{credentialsError}</Typography>
)} )}
{!credentialsLoading && !credentialsError && !credentials.length && ( {!useSvcAccount && !credentialsLoading && !credentialsError && !credentials.length && (
<Typography variant="body2" sx={{ color: "#ff8080" }}> <Typography variant="body2" sx={{ color: "#ff8080" }}>
No SSH or WinRM credentials available. Create one under Access Management. No SSH or WinRM credentials available. Create one under Access Management.
</Typography> </Typography>

View File

@@ -511,6 +511,7 @@ class JobScheduler:
scheduled_run_row_id: int, scheduled_run_row_id: int,
run_mode: str, run_mode: str,
credential_id: Optional[int] = None, credential_id: Optional[int] = None,
use_service_account: bool = False,
) -> Optional[Dict[str, Any]]: ) -> Optional[Dict[str, Any]]:
try: try:
import os, uuid import os, uuid
@@ -551,7 +552,7 @@ class JobScheduler:
server_run = run_mode_norm == "ssh" server_run = run_mode_norm == "ssh"
agent_winrm = run_mode_norm == "winrm" agent_winrm = run_mode_norm == "winrm"
if agent_winrm: if agent_winrm and not use_service_account:
if not credential_id: if not credential_id:
raise RuntimeError("WinRM execution requires a credential_id") raise RuntimeError("WinRM execution requires a credential_id")
if not callable(self._credential_fetcher): if not callable(self._credential_fetcher):
@@ -1000,7 +1001,7 @@ class JobScheduler:
pass pass
try: try:
cur.execute( cur.execute(
"SELECT id, components_json, targets_json, schedule_type, start_ts, expiration, execution_context, credential_id, created_at FROM scheduled_jobs WHERE enabled=1 ORDER BY id ASC" "SELECT id, components_json, targets_json, schedule_type, start_ts, expiration, execution_context, credential_id, use_service_account, created_at FROM scheduled_jobs WHERE enabled=1 ORDER BY id ASC"
) )
jobs = cur.fetchall() jobs = cur.fetchall()
except Exception: except Exception:
@@ -1018,7 +1019,18 @@ class JobScheduler:
five_min = 300 five_min = 300
now_min = _now_minute() now_min = _now_minute()
for (job_id, components_json, targets_json, schedule_type, start_ts, expiration, execution_context, credential_id, created_at) in jobs: for (
job_id,
components_json,
targets_json,
schedule_type,
start_ts,
expiration,
execution_context,
credential_id,
use_service_account_flag,
created_at,
) in jobs:
try: try:
# Targets list for this job # Targets list for this job
try: try:
@@ -1054,6 +1066,9 @@ class JobScheduler:
continue continue
run_mode = (execution_context or "system").strip().lower() run_mode = (execution_context or "system").strip().lower()
job_credential_id = None job_credential_id = None
job_use_service_account = bool(use_service_account_flag)
if run_mode != "winrm":
job_use_service_account = False
try: try:
job_credential_id = int(credential_id) if credential_id is not None else None job_credential_id = int(credential_id) if credential_id is not None else None
except Exception: except Exception:
@@ -1144,7 +1159,7 @@ class JobScheduler:
run_row_id = c2.lastrowid or 0 run_row_id = c2.lastrowid or 0
conn2.commit() conn2.commit()
activity_links: List[Dict[str, Any]] = [] activity_links: List[Dict[str, Any]] = []
remote_requires_cred = run_mode in ("ssh", "winrm") remote_requires_cred = (run_mode == "ssh") or (run_mode == "winrm" and not job_use_service_account)
if remote_requires_cred and not job_credential_id: if remote_requires_cred and not job_credential_id:
err_msg = "Credential required for remote execution" err_msg = "Credential required for remote execution"
c2.execute( c2.execute(
@@ -1178,6 +1193,7 @@ class JobScheduler:
run_row_id, run_row_id,
run_mode, run_mode,
job_credential_id, job_credential_id,
job_use_service_account,
) )
if link and link.get("activity_id"): if link and link.get("activity_id"):
activity_links.append({ activity_links.append({
@@ -1289,9 +1305,10 @@ class JobScheduler:
"expiration": r[7] or "no_expire", "expiration": r[7] or "no_expire",
"execution_context": r[8] or "system", "execution_context": r[8] or "system",
"credential_id": r[9], "credential_id": r[9],
"enabled": bool(r[10] or 0), "use_service_account": bool(r[10] or 0),
"created_at": r[11] or 0, "enabled": bool(r[11] or 0),
"updated_at": r[12] or 0, "created_at": r[12] or 0,
"updated_at": r[13] or 0,
} }
# Attach computed status summary for latest occurrence # Attach computed status summary for latest occurrence
try: try:
@@ -1368,7 +1385,8 @@ class JobScheduler:
cur.execute( cur.execute(
""" """
SELECT id, name, components_json, targets_json, schedule_type, start_ts, SELECT id, name, components_json, targets_json, schedule_type, start_ts,
duration_stop_enabled, expiration, execution_context, credential_id, enabled, created_at, updated_at duration_stop_enabled, expiration, execution_context, credential_id,
use_service_account, enabled, created_at, updated_at
FROM scheduled_jobs FROM scheduled_jobs
ORDER BY created_at DESC ORDER BY created_at DESC
""" """
@@ -1396,6 +1414,8 @@ class JobScheduler:
credential_id = int(credential_id) if credential_id is not None else None credential_id = int(credential_id) if credential_id is not None else None
except Exception: except Exception:
credential_id = None credential_id = None
use_service_account_raw = data.get("use_service_account")
use_service_account = 1 if (execution_context == "winrm" and (use_service_account_raw is None or bool(use_service_account_raw))) else 0
enabled = int(bool(data.get("enabled", True))) enabled = int(bool(data.get("enabled", True)))
if not name or not components or not targets: if not name or not components or not targets:
return json.dumps({"error": "name, components, targets required"}), 400, {"Content-Type": "application/json"} return json.dumps({"error": "name, components, targets required"}), 400, {"Content-Type": "application/json"}
@@ -1406,8 +1426,8 @@ class JobScheduler:
cur.execute( cur.execute(
""" """
INSERT INTO scheduled_jobs INSERT INTO scheduled_jobs
(name, components_json, targets_json, schedule_type, start_ts, duration_stop_enabled, expiration, execution_context, credential_id, enabled, created_at, updated_at) (name, components_json, targets_json, schedule_type, start_ts, duration_stop_enabled, expiration, execution_context, credential_id, use_service_account, enabled, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
""", """,
( (
name, name,
@@ -1419,6 +1439,7 @@ class JobScheduler:
expiration, expiration,
execution_context, execution_context,
credential_id, credential_id,
use_service_account,
enabled, enabled,
now, now,
now, now,
@@ -1429,7 +1450,7 @@ class JobScheduler:
cur.execute( cur.execute(
""" """
SELECT id, name, components_json, targets_json, schedule_type, start_ts, SELECT id, name, components_json, targets_json, schedule_type, start_ts,
duration_stop_enabled, expiration, execution_context, credential_id, enabled, created_at, updated_at duration_stop_enabled, expiration, execution_context, credential_id, use_service_account, enabled, created_at, updated_at
FROM scheduled_jobs WHERE id=? FROM scheduled_jobs WHERE id=?
""", """,
(job_id,), (job_id,),
@@ -1448,7 +1469,7 @@ class JobScheduler:
cur.execute( cur.execute(
""" """
SELECT id, name, components_json, targets_json, schedule_type, start_ts, SELECT id, name, components_json, targets_json, schedule_type, start_ts,
duration_stop_enabled, expiration, execution_context, credential_id, enabled, created_at, updated_at duration_stop_enabled, expiration, execution_context, credential_id, use_service_account, enabled, created_at, updated_at
FROM scheduled_jobs WHERE id=? FROM scheduled_jobs WHERE id=?
""", """,
(job_id,), (job_id,),
@@ -1481,7 +1502,10 @@ class JobScheduler:
if "expiration" in data or (data.get("duration") and "expiration" in data.get("duration")): if "expiration" in data or (data.get("duration") and "expiration" in data.get("duration")):
fields["expiration"] = (data.get("duration") or {}).get("expiration") or data.get("expiration") or "no_expire" fields["expiration"] = (data.get("duration") or {}).get("expiration") or data.get("expiration") or "no_expire"
if "execution_context" in data: if "execution_context" in data:
fields["execution_context"] = (data.get("execution_context") or "system").strip().lower() exec_ctx_val = (data.get("execution_context") or "system").strip().lower()
fields["execution_context"] = exec_ctx_val
if exec_ctx_val != "winrm":
fields["use_service_account"] = 0
if "credential_id" in data: if "credential_id" in data:
cred_val = data.get("credential_id") cred_val = data.get("credential_id")
if cred_val in (None, "", "null"): if cred_val in (None, "", "null"):
@@ -1491,6 +1515,8 @@ class JobScheduler:
fields["credential_id"] = int(cred_val) fields["credential_id"] = int(cred_val)
except Exception: except Exception:
fields["credential_id"] = None fields["credential_id"] = None
if "use_service_account" in data:
fields["use_service_account"] = 1 if bool(data.get("use_service_account")) else 0
if "enabled" in data: if "enabled" in data:
fields["enabled"] = int(bool(data.get("enabled"))) fields["enabled"] = int(bool(data.get("enabled")))
if not fields: if not fields:
@@ -1508,7 +1534,7 @@ class JobScheduler:
cur.execute( cur.execute(
""" """
SELECT id, name, components_json, targets_json, schedule_type, start_ts, SELECT id, name, components_json, targets_json, schedule_type, start_ts,
duration_stop_enabled, expiration, execution_context, credential_id, enabled, created_at, updated_at duration_stop_enabled, expiration, execution_context, credential_id, use_service_account, enabled, created_at, updated_at
FROM scheduled_jobs WHERE id=? FROM scheduled_jobs WHERE id=?
""", """,
(job_id,), (job_id,),
@@ -1532,7 +1558,7 @@ class JobScheduler:
return json.dumps({"error": "not found"}), 404, {"Content-Type": "application/json"} return json.dumps({"error": "not found"}), 404, {"Content-Type": "application/json"}
conn.commit() conn.commit()
cur.execute( cur.execute(
"SELECT id, name, components_json, targets_json, schedule_type, start_ts, duration_stop_enabled, expiration, execution_context, credential_id, enabled, created_at, updated_at FROM scheduled_jobs WHERE id=?", "SELECT id, name, components_json, targets_json, schedule_type, start_ts, duration_stop_enabled, expiration, execution_context, credential_id, use_service_account, enabled, created_at, updated_at FROM scheduled_jobs WHERE id=?",
(job_id,), (job_id,),
) )
row = cur.fetchone() row = cur.fetchone()

View File

@@ -4531,6 +4531,7 @@ def init_db():
expiration TEXT, expiration TEXT,
execution_context TEXT NOT NULL, execution_context TEXT NOT NULL,
credential_id INTEGER, credential_id INTEGER,
use_service_account INTEGER NOT NULL DEFAULT 1,
enabled INTEGER DEFAULT 1, enabled INTEGER DEFAULT 1,
created_at INTEGER, created_at INTEGER,
updated_at INTEGER updated_at INTEGER
@@ -4542,6 +4543,8 @@ def init_db():
sj_cols = [row[1] for row in cur.fetchall()] sj_cols = [row[1] for row in cur.fetchall()]
if "credential_id" not in sj_cols: if "credential_id" not in sj_cols:
cur.execute("ALTER TABLE scheduled_jobs ADD COLUMN credential_id INTEGER") cur.execute("ALTER TABLE scheduled_jobs ADD COLUMN credential_id INTEGER")
if "use_service_account" not in sj_cols:
cur.execute("ALTER TABLE scheduled_jobs ADD COLUMN use_service_account INTEGER NOT NULL DEFAULT 1")
except Exception: except Exception:
pass pass
conn.commit() conn.commit()
@@ -6410,12 +6413,21 @@ def ansible_quick_run():
rel_path = (data.get("playbook_path") or "").strip() rel_path = (data.get("playbook_path") or "").strip()
hostnames = data.get("hostnames") or [] hostnames = data.get("hostnames") or []
credential_id = data.get("credential_id") credential_id = data.get("credential_id")
use_service_account_raw = data.get("use_service_account")
if not rel_path or not isinstance(hostnames, list) or not hostnames: if not rel_path or not isinstance(hostnames, list) or not hostnames:
_ansible_log_server(f"[quick_run] invalid payload rel_path='{rel_path}' hostnames={hostnames}") _ansible_log_server(f"[quick_run] invalid payload rel_path='{rel_path}' hostnames={hostnames}")
return jsonify({"error": "Missing playbook_path or hostnames[]"}), 400 return jsonify({"error": "Missing playbook_path or hostnames[]"}), 400
server_mode = False server_mode = False
cred_id_int = None cred_id_int = None
credential_detail: Optional[Dict[str, Any]] = None credential_detail: Optional[Dict[str, Any]] = None
overrides_raw = data.get("variable_values")
variable_values: Dict[str, Any] = {}
if isinstance(overrides_raw, dict):
for key, val in overrides_raw.items():
name = str(key or "").strip()
if not name:
continue
variable_values[name] = val
if credential_id not in (None, "", "null"): if credential_id not in (None, "", "null"):
try: try:
cred_id_int = int(credential_id) cred_id_int = int(credential_id)
@@ -6423,7 +6435,13 @@ def ansible_quick_run():
cred_id_int = None cred_id_int = None
except Exception: except Exception:
return jsonify({"error": "Invalid credential_id"}), 400 return jsonify({"error": "Invalid credential_id"}), 400
if use_service_account_raw is None:
use_service_account = cred_id_int is None
else:
use_service_account = bool(use_service_account_raw)
if use_service_account:
cred_id_int = None
credential_detail = None
if cred_id_int: if cred_id_int:
credential_detail = _fetch_credential_with_secrets(cred_id_int) credential_detail = _fetch_credential_with_secrets(cred_id_int)
if not credential_detail: if not credential_detail:
@@ -6446,15 +6464,6 @@ def ansible_quick_run():
variables = doc.get('variables') if isinstance(doc.get('variables'), list) else [] variables = doc.get('variables') if isinstance(doc.get('variables'), list) else []
files = doc.get('files') if isinstance(doc.get('files'), list) else [] files = doc.get('files') if isinstance(doc.get('files'), list) else []
friendly_name = (doc.get("name") or "").strip() or os.path.basename(abs_path) friendly_name = (doc.get("name") or "").strip() or os.path.basename(abs_path)
overrides_raw = data.get("variable_values")
variable_values = {}
if isinstance(overrides_raw, dict):
for key, val in overrides_raw.items():
name = str(key or "").strip()
if not name:
continue
variable_values[name] = val
if server_mode and not cred_id_int: if server_mode and not cred_id_int:
return jsonify({"error": "credential_id is required for server-side execution"}), 400 return jsonify({"error": "credential_id is required for server-side execution"}), 400