mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-27 03:41:57 -06:00
Allow selecting svcBorealis account for playbooks
This commit is contained in:
@@ -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'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 > Credentials.
|
No {execContext === "winrm" ? "WinRM" : "SSH"} credentials available. Create one under Access Management > Credentials.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -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'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>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user