Files
Server_Monitorizare_v2/templates/ansible/execute.html

527 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Execute Ansible Playbook - Server Monitoring{% endblock %}
{% block page_title %}Execute Ansible Playbook{% endblock %}
{% block extra_css %}
<style>
.playbook-card {
border: 2px solid #e9ecef; border-radius: 8px;
transition: all 0.2s; cursor: pointer;
}
.playbook-card:hover { border-color: #0d6efd; box-shadow: 0 3px 10px rgba(13,110,253,.15); }
.playbook-card.selected { border-color: #198754; background-color: #f8fff9; }
.json-editor { font-family: 'Courier New', monospace; background-color: #f8f9fa; }
#execPopupNotice { display: none; }
.pulse-dot {
width: 10px; height: 10px; border-radius: 50%;
background: #4ec9b0; animation: pulse 1.2s infinite; display: inline-block;
}
.pulse-dot.done { background: #6a9955; animation: none; }
.pulse-dot.error { background: #f44747; animation: none; }
@keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:.3;} }
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category=='error' else category }} alert-dismissible fade show">
{{ message }}<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}{% endif %}
{% endwith %}
<form method="POST" id="executeForm">
<div class="row g-3 mb-3">
<!-- ── Playbook selection ──────────────────────────────────── -->
<div class="col-lg-5">
<div class="card h-100">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-book me-2"></i>Select Playbook</h5>
</div>
<div class="card-body" style="overflow-y:auto; max-height:480px;">
<h6 class="text-muted fw-bold mb-2 small text-uppercase">Built-in</h6>
<div class="card playbook-card mb-2" data-name="update_devices" onclick="selectPlaybook('update_devices')">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1 text-primary"><i class="fas fa-download me-1"></i>Update Devices</h6>
<p class="small text-muted mb-0">Update all packages on monitoring devices</p>
</div>
<span class="badge bg-success ms-2 flex-shrink-0">Built-in</span>
</div>
</div>
</div>
<div class="card playbook-card mb-2" data-name="restart_service" onclick="selectPlaybook('restart_service')">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1 text-success"><i class="fas fa-redo me-1"></i>Restart Service</h6>
<p class="small text-muted mb-0">Restart monitoring services on devices</p>
</div>
<span class="badge bg-success ms-2 flex-shrink-0">Built-in</span>
</div>
</div>
</div>
<div class="card playbook-card mb-3" data-name="system_health" onclick="selectPlaybook('system_health')">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1 text-info"><i class="fas fa-heartbeat me-1"></i>System Health</h6>
<p class="small text-muted mb-0">Check system health and monitoring status</p>
</div>
<span class="badge bg-success ms-2 flex-shrink-0">Built-in</span>
</div>
</div>
</div>
<div class="card playbook-card mb-3" data-name="distribute_ssh_keys" onclick="selectPlaybook('distribute_ssh_keys')">
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1 text-warning"><i class="fas fa-key me-1"></i>Distribute SSH Keys</h6>
<p class="small text-muted mb-0">Push server public key to devices using password auth</p>
</div>
<span class="badge bg-warning text-dark ms-2 flex-shrink-0">Built-in</span>
</div>
</div>
</div>
<h6 class="text-muted fw-bold mb-2 small text-uppercase">Custom Playbooks</h6>
<select class="form-select" id="customPlaybook">
<option value="">— select custom playbook —</option>
</select>
<input type="hidden" name="playbook" id="selectedPlaybook">
</div>
</div>
</div>
<!-- ── Target selection ────────────────────────────────────── -->
<div class="col-lg-7">
<div class="card h-100">
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-crosshairs me-2"></i>Select Target</h5>
<span id="targetCountBadge" class="badge bg-light text-dark"></span>
</div>
<div class="card-body p-3">
<!-- Mode pills -->
<ul class="nav nav-pills nav-fill mb-3">
<li class="nav-item">
<button type="button" class="nav-link active" id="pill-all" onclick="setTargetMode('all')">
<i class="fas fa-globe me-1"></i>All Hosts
</button>
</li>
<li class="nav-item">
<button type="button" class="nav-link" id="pill-group" onclick="setTargetMode('group')">
<i class="fas fa-layer-group me-1"></i>By Group
</button>
</li>
<li class="nav-item">
<button type="button" class="nav-link" id="pill-host" onclick="setTargetMode('host')">
<i class="fas fa-server me-1"></i>By Host
</button>
</li>
</ul>
<!-- All Hosts panel -->
<div id="panel-all">
{% set ns = namespace(total=0) %}
{% for g in inventory.groups.values() %}{% set ns.total = ns.total + g.hosts|length %}{% endfor %}
{% if ns.total > 0 %}
<div class="alert alert-info mb-0">
<i class="fas fa-info-circle me-2"></i>
Will run on <strong>all {{ ns.total }} host(s)</strong>
across <strong>{{ inventory.groups|length }} group(s)</strong>.
</div>
{% else %}
<div class="alert alert-warning mb-0">
<i class="fas fa-exclamation-triangle me-2"></i>
No hosts in inventory yet.
<a href="{{ url_for('ansible_web.devices') }}">Add devices to inventory</a> first.
</div>
{% endif %}
</div>
<!-- By Group panel -->
<div id="panel-group" class="d-none" style="max-height:300px;overflow-y:auto;">
{% if inventory.groups %}
{% for group_name, group in inventory.groups.items() %}
<div class="form-check border rounded p-2 mb-2">
<input class="form-check-input group-cb" type="checkbox"
id="grp-{{ group_name }}" value="{{ group_name }}"
data-count="{{ group.hosts|length }}"
onchange="updateTargetCount()">
<label class="form-check-label w-100" for="grp-{{ group_name }}">
<div class="d-flex justify-content-between align-items-center">
<strong>{{ group_name }}</strong>
<span class="badge bg-secondary">{{ group.hosts|length }} host(s)</span>
</div>
<small class="text-muted">
{% for h in group.hosts[:3] %}{{ h.hostname }}{% if not loop.last %}, {% endif %}{% endfor %}{% if group.hosts|length > 3 %}&nbsp;+{{ group.hosts|length - 3 }} more{% endif %}
</small>
</label>
</div>
{% endfor %}
{% else %}
<p class="text-muted text-center py-3">No groups in inventory.</p>
{% endif %}
</div>
<!-- By Host panel -->
<div id="panel-host" class="d-none" style="max-height:300px;overflow-y:auto;">
{% if all_inv_hosts %}
<div class="mb-2 d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="checkAllHosts(true)">
<i class="fas fa-check-double me-1"></i>Select All
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="checkAllHosts(false)">Clear</button>
</div>
{% for h in all_inv_hosts %}
<div class="form-check border-bottom py-2">
<input class="form-check-input host-cb" type="checkbox"
id="invhost-{{ h.hostname }}" value="{{ h.hostname }}"
onchange="updateTargetCount()">
<label class="form-check-label d-flex justify-content-between w-100" for="invhost-{{ h.hostname }}">
<strong>{{ h.hostname }}</strong>
<code class="text-muted" style="font-size:.8rem;">{{ h.ip }}</code>
</label>
</div>
{% endfor %}
{% else %}
<p class="text-muted text-center py-3">No hosts in inventory yet.</p>
{% endif %}
</div>
</div>
</div>
</div>
</div><!-- /top row -->
<!-- ── Advanced Options ──────────────────────────────────────── -->
<div class="row mb-3">
<div class="col-12">
<div class="card">
<div class="card-header bg-info text-white">
<h6 class="mb-0">
<i class="fas fa-cogs me-1"></i>Advanced Options
<button type="button" class="btn btn-outline-light btn-sm ms-2"
data-bs-toggle="collapse" data-bs-target="#advancedOptions">
<i class="fas fa-chevron-down"></i>
</button>
</h6>
</div>
<div class="collapse" id="advancedOptions">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<h6>Execution Settings</h6>
<div class="mb-3">
<label class="form-label">Priority</label>
<select class="form-select" name="priority">
<option value="1">1 — Low</option>
<option value="3">3 — Below Normal</option>
<option value="5" selected>5 — Normal</option>
<option value="7">7 — High</option>
<option value="10">10 — Critical</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Max Retries</label>
<select class="form-select" name="max_retries">
<option value="0" selected>0 — No retries</option>
<option value="1">1 retry</option>
<option value="2">2 retries</option>
<option value="3">3 retries</option>
</select>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="checkMode" name="check_mode">
<label class="form-check-label" for="checkMode">Dry run (check mode)</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="forcePasswordAuth"
name="force_password_auth" role="switch"
{% if use_password_auth %}checked{% endif %}>
<label class="form-check-label fw-semibold" for="forcePasswordAuth">
<i class="fas fa-lock me-1 text-warning"></i>Use password authentication
</label>
<div class="text-muted small mt-1">
Override SSH key auth and connect with the configured device password.
Auto-enabled for <em>Distribute SSH Keys</em>.
</div>
</div>
</div>
<div class="col-md-4">
<label class="form-label">Extra Variables (JSON)</label>
<textarea class="form-control json-editor" name="extra_vars" id="extraVars" rows="6"
placeholder='{"variable": "value"}'></textarea>
<div class="form-text">Optional variables passed to playbook</div>
</div>
<div class="col-md-4">
<h6>Common Variables</h6>
<ul class="list-unstyled small">
<li><code>timeout: 600</code></li>
<li><code>reboot_timeout: 900</code></li>
<li><code>become_user: "root"</code></li>
<li><code>force: true</code></li>
</ul>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="addCommonVars()">
<i class="fas fa-plus me-1"></i>Add Common Vars
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ── Summary bar + Execute button ─────────────────────────── -->
<div class="card mb-3">
<div class="card-body py-3">
<div class="row align-items-center">
<div class="col-md-3">
<small class="text-muted d-block">Playbook</small>
<strong id="summaryPlaybook">None selected</strong>
</div>
<div class="col-md-4">
<small class="text-muted d-block">Target</small>
<strong id="summaryDevices">None selected</strong>
</div>
<div class="col-md-3">
<small class="text-muted d-block">Est. Duration</small>
<strong id="estimatedDuration">Unknown</strong>
</div>
<div class="col-md-2 text-end">
<button type="submit" class="btn btn-primary btn-lg w-100" id="executeButton">
<i class="fas fa-play me-1"></i>Execute
</button>
</div>
</div>
</div>
</div>
<!-- Hidden host inputs populated by JS before submit -->
<div id="hostsContainer"></div>
</form>
<!-- ── Popup launched notice ─────────────────────────────────── -->
<div class="row mt-3" id="execPopupNotice">
<div class="col-12">
<div class="alert alert-info d-flex align-items-center gap-3 mb-0">
<span class="pulse-dot" id="noticePulseDot"></span>
<div class="flex-grow-1">
<strong>Execution started!</strong>
A live output window has been opened.
If it was blocked by your browser, use the link below.
</div>
<a id="noticePopupLink" href="#" target="_blank" class="btn btn-sm btn-outline-primary flex-shrink-0">
<i class="fas fa-external-link-alt me-1"></i>Open Live Output
</a>
<a id="noticeDetailsLink" href="#" class="btn btn-sm btn-outline-secondary flex-shrink-0">
<i class="fas fa-list me-1"></i>Full Details
</a>
</div>
</div>
</div>
</div><!-- /container-fluid -->
<script>
// ── State ────────────────────────────────────────────────────────────
let selectedPlaybook = null;
let targetMode = 'all';
let executionId = null;
// ── Playbook selection ───────────────────────────────────────────────
function selectPlaybook(name) {
document.querySelectorAll('.playbook-card').forEach(c => c.classList.remove('selected'));
document.getElementById('customPlaybook').value = '';
document.querySelector(`.playbook-card[data-name="${name}"]`)?.classList.add('selected');
selectedPlaybook = name;
document.getElementById('selectedPlaybook').value = name;
// Auto-enable password auth for the key distribution playbook
const pwdToggle = document.getElementById('forcePasswordAuth');
if (pwdToggle) {
if (name === 'distribute_ssh_keys') {
pwdToggle.checked = true;
}
}
updateSummary();
}
document.getElementById('customPlaybook').addEventListener('change', function() {
if (this.value) {
document.querySelectorAll('.playbook-card').forEach(c => c.classList.remove('selected'));
selectedPlaybook = this.value;
document.getElementById('selectedPlaybook').value = this.value;
updateSummary();
}
});
// ── Target mode ──────────────────────────────────────────────────────
function setTargetMode(mode) {
targetMode = mode;
['all','group','host'].forEach(m => {
document.getElementById(`panel-${m}`).classList.toggle('d-none', m !== mode);
document.getElementById(`pill-${m}`).classList.toggle('active', m === mode);
});
updateTargetCount();
}
function updateTargetCount() {
let count = 0, label = '';
if (targetMode === 'all') {
count = document.querySelectorAll('.host-cb').length;
label = count > 0 ? `All — ${count} host(s)` : 'No hosts in inventory';
} else if (targetMode === 'group') {
const grps = document.querySelectorAll('.group-cb:checked');
grps.forEach(cb => count += parseInt(cb.dataset.count || '0'));
label = grps.length > 0 ? `${grps.length} group(s) / ${count} host(s)` : 'None selected';
} else {
count = document.querySelectorAll('.host-cb:checked').length;
label = count > 0 ? `${count} host(s) selected` : 'None selected';
}
document.getElementById('targetCountBadge').textContent = label;
updateSummary();
}
function checkAllHosts(check) {
document.querySelectorAll('.host-cb').forEach(cb => cb.checked = check);
updateTargetCount();
}
function getTargetCount() {
if (targetMode === 'all') return document.querySelectorAll('.host-cb').length;
if (targetMode === 'group') {
let c = 0;
document.querySelectorAll('.group-cb:checked').forEach(cb => c += parseInt(cb.dataset.count || '0'));
return c;
}
return document.querySelectorAll('.host-cb:checked').length;
}
// Populate hidden <input name="hosts"> elements before submit
function buildHiddenInputs() {
const container = document.getElementById('hostsContainer');
container.innerHTML = '';
const add = v => {
const inp = document.createElement('input');
inp.type = 'hidden'; inp.name = 'hosts'; inp.value = v;
container.appendChild(inp);
};
if (targetMode === 'all') {
document.querySelectorAll('.host-cb').forEach(cb => add(cb.value));
if (container.children.length === 0) add('all'); // fallback if inventory empty
} else if (targetMode === 'group') {
document.querySelectorAll('.group-cb:checked').forEach(cb => add(cb.value));
} else {
document.querySelectorAll('.host-cb:checked').forEach(cb => add(cb.value));
}
}
// ── Summary bar ──────────────────────────────────────────────────────
function updateSummary() {
document.getElementById('summaryPlaybook').textContent = selectedPlaybook || 'None selected';
const count = getTargetCount();
const sd = document.getElementById('summaryDevices');
if (targetMode === 'all') {
sd.textContent = count > 0 ? `All hosts (${count})` : 'No hosts in inventory';
} else if (targetMode === 'group') {
const g = document.querySelectorAll('.group-cb:checked').length;
sd.textContent = g > 0 ? `${g} group(s), ~${count} host(s)` : 'None selected';
} else {
sd.textContent = count > 0 ? `${count} host(s) selected` : 'None selected';
}
const dur = document.getElementById('estimatedDuration');
if (selectedPlaybook && count > 0) {
if (selectedPlaybook === 'update_devices')
dur.textContent = `${Math.max(5, count * 2)}${count * 10} min`;
else if (selectedPlaybook === 'restart_service')
dur.textContent = `${count}${count * 2} min`;
else dur.textContent = 'Varies';
} else { dur.textContent = 'Unknown'; }
}
function addCommonVars() {
const el = document.getElementById('extraVars');
const common = {"timeout": 600, "reboot_timeout": 900, "become_user": "root"};
try {
let cur = el.value.trim() ? JSON.parse(el.value) : {};
Object.assign(cur, common);
el.value = JSON.stringify(cur, null, 2);
} catch { el.value = JSON.stringify(common, null, 2); }
}
// ── Form submission ──────────────────────────────────────────────────
document.getElementById('executeForm').addEventListener('submit', function(e) {
e.preventDefault();
if (!selectedPlaybook) { alert('Please select a playbook'); return; }
const count = getTargetCount();
if (count === 0) { alert('Please select at least one host or group'); return; }
const extraRaw = document.getElementById('extraVars').value.trim();
if (extraRaw) {
try { JSON.parse(extraRaw); }
catch { alert('Invalid JSON in Extra Variables'); return; }
}
const grpCount = document.querySelectorAll('.group-cb:checked').length;
const targetDesc = targetMode === 'all' ? `all hosts (${count})` :
targetMode === 'group' ? `${grpCount} group(s)` :
`${count} host(s)`;
if (!confirm(`Execute "${selectedPlaybook}" on ${targetDesc}?\nThis cannot be undone.`)) return;
buildHiddenInputs();
const btn = document.getElementById('executeButton');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Starting…';
fetch(this.action, {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
body: new FormData(this),
})
.then(r => r.json())
.then(data => {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-play me-1"></i>Execute';
if (data.success) {
executionId = data.execution_id;
const popupUrl = `/ansible/executions/${executionId}/live-popup`;
const detailUrl = `/ansible/executions/${executionId}`;
// Open independent popup window
window.open(popupUrl, `exec_${executionId}`,
'width=960,height=660,resizable=yes,scrollbars=no,toolbar=no,menubar=no');
// Show notice bar with fallback links
const notice = document.getElementById('execPopupNotice');
notice.style.display = '';
document.getElementById('noticePopupLink').href = popupUrl;
document.getElementById('noticeDetailsLink').href = detailUrl;
document.getElementById('noticePulseDot').className = 'pulse-dot';
} else {
alert('Execution failed: ' + (data.error || 'Unknown error'));
}
})
.catch(err => {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-play me-1"></i>Execute';
alert('Network error: ' + err);
});
});
// ── Initialize ───────────────────────────────────────────────────────
updateTargetCount();
updateSummary();
{% if preselect_playbook %}
selectPlaybook('{{ preselect_playbook }}');
{% endif %}
</script>
{% endblock %}