feat(netbird): central control of client Automatic Updates across all customers
Lets the MSP admin control NetBird's own "Settings > Clients > Automatic Updates" feature (client/peer auto-update, v0.61.0+) for every customer from one place, instead of logging into each customer's dashboard individually. - New deployments automatically capture a Personal Access Token during the existing /api/setup bootstrap call (create_pat=true), requiring NB_SETUP_PAT_ENABLED=true on the management container (now set by default in the compose template). Token is encrypted at rest per customer. - Existing customers (deployed before this existed) can have a token pasted in manually from their own dashboard — verified before being stored. - Settings > Docker Images: master default (version + force-update toggle) plus "Apply to All Customers" which pushes it to everyone with a token. - Customer detail page: shows the customer's live current setting (read from their NetBird API, not cached) with per-customer override or "sync from default". - New app/services/netbird_client_update_service.py wraps the customer's NetBird Management API (GET/PUT /api/accounts) for this.
This commit is contained in:
@@ -668,6 +668,33 @@
|
||||
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-save me-1"></i><span data-i18n="monitoring.saveAutoUpdateSettings">Save Automation</span></button>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
<h6 data-i18n="customer.nbuMasterTitle">NetBird Client Auto-Updates (all customers)</h6>
|
||||
<p class="text-muted small" data-i18n="customer.nbuMasterHint">Controls the "Automatic Updates" setting inside every customer's own NetBird dashboard (Settings > Clients). Set the default here, then push it to all customers at once. Individual customers can still be overridden from their detail page.</p>
|
||||
<form id="settings-nbu-master-form">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label class="form-label small mb-1" data-i18n="customer.nbuVersion">Client version</label>
|
||||
<select class="form-select form-select-sm" id="cfg-nbu-version-select" onchange="document.getElementById('cfg-nbu-custom-version').classList.toggle('d-none', this.value !== 'custom')">
|
||||
<option value="disabled" data-i18n="customer.nbuDisabled">Disabled</option>
|
||||
<option value="latest" data-i18n="customer.nbuLatest">Latest</option>
|
||||
<option value="custom" data-i18n="customer.nbuCustom">Specific version</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<input type="text" class="form-control form-control-sm d-none" id="cfg-nbu-custom-version" placeholder="0.61.0">
|
||||
</div>
|
||||
<div class="col-auto form-check pb-1">
|
||||
<input class="form-check-input" type="checkbox" id="cfg-nbu-always">
|
||||
<label class="form-check-label small" for="cfg-nbu-always" data-i18n="customer.nbuForce">Force automatic updates</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button type="submit" class="btn btn-primary btn-sm me-2"><i class="bi bi-save me-1"></i><span data-i18n="customer.nbuSaveDefault">Save Default</span></button>
|
||||
<button type="button" class="btn btn-outline-warning btn-sm" id="btn-nbu-apply-all" onclick="applyNetbirdUpdatesToAll()"><i class="bi bi-broadcast me-1"></i><span data-i18n="customer.nbuApplyAll">Apply to All Customers</span></button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="nbu-apply-all-result" class="mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -571,6 +571,121 @@ function goToPage(page) {
|
||||
loadCustomers();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NetBird client (peer) automatic-updates — per-customer
|
||||
// ---------------------------------------------------------------------------
|
||||
function _nbuVersionOptions(selected) {
|
||||
const opts = [
|
||||
['disabled', t('customer.nbuDisabled')],
|
||||
['latest', t('customer.nbuLatest')],
|
||||
['custom', t('customer.nbuCustom')],
|
||||
];
|
||||
const isCustom = selected && selected !== 'disabled' && selected !== 'latest';
|
||||
return opts.map(([v, label]) =>
|
||||
`<option value="${v}" ${(!isCustom && v === selected) || (isCustom && v === 'custom') ? 'selected' : ''}>${label}</option>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
async function loadCustomerNetbirdUpdates(id, hasToken) {
|
||||
const container = document.getElementById('nbu-container');
|
||||
if (!container) return;
|
||||
|
||||
if (!hasToken) {
|
||||
container.innerHTML = `
|
||||
<p class="text-muted small mb-2">${t('customer.nbuNoToken')}</p>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" id="nbu-token-input" placeholder="${t('customer.nbuTokenPlaceholder')}">
|
||||
<button class="btn btn-outline-primary" onclick="saveCustomerNetbirdToken(${id})">${t('customer.nbuSaveToken')}</button>
|
||||
</div>
|
||||
<div id="nbu-token-result" class="small mt-1"></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `<span class="spinner-border spinner-border-sm"></span>`;
|
||||
try {
|
||||
const data = await api('GET', `/customers/${id}/netbird-updates`);
|
||||
const isCustom = data.version && data.version !== 'disabled' && data.version !== 'latest';
|
||||
container.innerHTML = `
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-auto">
|
||||
<label class="form-label small mb-1">${t('customer.nbuVersion')}</label>
|
||||
<select class="form-select form-select-sm" id="nbu-version-select" onchange="document.getElementById('nbu-custom-version').classList.toggle('d-none', this.value !== 'custom')">
|
||||
${_nbuVersionOptions(data.version)}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<input type="text" class="form-control form-control-sm ${isCustom ? '' : 'd-none'}" id="nbu-custom-version" placeholder="0.61.0" value="${isCustom ? esc(data.version) : ''}">
|
||||
</div>
|
||||
<div class="col-auto form-check pb-1">
|
||||
<input class="form-check-input" type="checkbox" id="nbu-always" ${data.always ? 'checked' : ''}>
|
||||
<label class="form-check-label small" for="nbu-always">${t('customer.nbuForce')}</label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-primary btn-sm" onclick="saveCustomerNetbirdUpdate(${id})">${t('customer.nbuSave')}</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="syncCustomerNetbirdFromMaster(${id})">${t('customer.nbuSyncMaster')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="nbu-result" class="small mt-2"></div>`;
|
||||
} catch (err) {
|
||||
container.innerHTML = `<div class="alert alert-warning py-2 small mb-0">${esc(err.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCustomerNetbirdToken(id) {
|
||||
const input = document.getElementById('nbu-token-input');
|
||||
const resultEl = document.getElementById('nbu-token-result');
|
||||
const token = input.value.trim();
|
||||
if (!token) return;
|
||||
resultEl.innerHTML = `<span class="spinner-border spinner-border-sm"></span>`;
|
||||
try {
|
||||
await api('PUT', `/customers/${id}/netbird-api-token`, { token });
|
||||
showToast(t('customer.nbuTokenSaved'));
|
||||
loadCustomerNetbirdUpdates(id, true);
|
||||
} catch (err) {
|
||||
resultEl.innerHTML = `<span class="text-danger">${esc(err.message)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function _readNbuForm() {
|
||||
const select = document.getElementById('nbu-version-select').value;
|
||||
const version = select === 'custom' ? document.getElementById('nbu-custom-version').value.trim() : select;
|
||||
const always = document.getElementById('nbu-always').checked;
|
||||
return { version, always };
|
||||
}
|
||||
|
||||
async function saveCustomerNetbirdUpdate(id) {
|
||||
const resultEl = document.getElementById('nbu-result');
|
||||
const payload = await _readNbuForm();
|
||||
if (!payload.version) {
|
||||
resultEl.innerHTML = `<span class="text-danger">${t('customer.nbuVersionRequired')}</span>`;
|
||||
return;
|
||||
}
|
||||
resultEl.innerHTML = `<span class="spinner-border spinner-border-sm"></span>`;
|
||||
try {
|
||||
await api('PUT', `/customers/${id}/netbird-updates`, payload);
|
||||
showToast(t('customer.nbuSaved'));
|
||||
loadCustomerNetbirdUpdates(id, true);
|
||||
} catch (err) {
|
||||
resultEl.innerHTML = `<span class="text-danger">${esc(err.message)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function syncCustomerNetbirdFromMaster(id) {
|
||||
const resultEl = document.getElementById('nbu-result');
|
||||
resultEl.innerHTML = `<span class="spinner-border spinner-border-sm"></span>`;
|
||||
try {
|
||||
const cfg = await api('GET', '/settings/system');
|
||||
await api('PUT', `/customers/${id}/netbird-updates`, {
|
||||
version: cfg.netbird_client_auto_update_version,
|
||||
always: cfg.netbird_client_auto_update_always,
|
||||
});
|
||||
showToast(t('customer.nbuSynced'));
|
||||
loadCustomerNetbirdUpdates(id, true);
|
||||
} catch (err) {
|
||||
resultEl.innerHTML = `<span class="text-danger">${esc(err.message)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Search & filter listeners
|
||||
document.getElementById('search-input').addEventListener('input', debounce(() => { customersPage = 1; loadCustomers(); }, 300));
|
||||
document.getElementById('status-filter').addEventListener('change', () => { customersPage = 1; loadCustomers(); });
|
||||
@@ -812,6 +927,14 @@ async function viewCustomer(id) {
|
||||
` : `<p class="text-muted mb-0">${t('customer.credentialsNotAvailable')}</p>`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<strong><i class="bi bi-phone me-1"></i>${t('customer.netbirdClientUpdates')}</strong>
|
||||
</div>
|
||||
<div class="card-body" id="nbu-container">
|
||||
<span class="spinner-border spinner-border-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-success btn-sm me-1" onclick="customerAction(${id},'start')"><i class="bi bi-play-circle me-1"></i>${t('customer.start')}</button>
|
||||
<button class="btn btn-warning btn-sm me-1" onclick="customerAction(${id},'stop')"><i class="bi bi-stop-circle me-1"></i>${t('customer.stop')}</button>
|
||||
@@ -824,6 +947,7 @@ async function viewCustomer(id) {
|
||||
</div>
|
||||
<div id="detail-update-result"></div>
|
||||
`;
|
||||
loadCustomerNetbirdUpdates(id, d.has_netbird_api_token);
|
||||
} else {
|
||||
document.getElementById('detail-deployment-content').innerHTML = `
|
||||
<p class="text-muted">${t('customer.noDeployment')}</p>
|
||||
@@ -947,6 +1071,13 @@ async function loadSettings() {
|
||||
? new Date(cfg.auto_update_last_run_at).toLocaleString()
|
||||
: t('monitoring.autoUpdateNever');
|
||||
|
||||
const nbuVersion = cfg.netbird_client_auto_update_version || 'disabled';
|
||||
const nbuIsCustom = nbuVersion !== 'disabled' && nbuVersion !== 'latest';
|
||||
document.getElementById('cfg-nbu-version-select').value = nbuIsCustom ? 'custom' : nbuVersion;
|
||||
document.getElementById('cfg-nbu-custom-version').value = nbuIsCustom ? nbuVersion : '';
|
||||
document.getElementById('cfg-nbu-custom-version').classList.toggle('d-none', !nbuIsCustom);
|
||||
document.getElementById('cfg-nbu-always').checked = cfg.netbird_client_auto_update_always || false;
|
||||
|
||||
// Branding tab
|
||||
document.getElementById('cfg-branding-name').value = cfg.branding_name || '';
|
||||
document.getElementById('cfg-branding-subtitle').value = cfg.branding_subtitle || '';
|
||||
@@ -1080,6 +1211,67 @@ document.getElementById('settings-auto-update-form').addEventListener('submit',
|
||||
}
|
||||
});
|
||||
|
||||
function _readNbuMasterForm() {
|
||||
const select = document.getElementById('cfg-nbu-version-select').value;
|
||||
const version = select === 'custom' ? document.getElementById('cfg-nbu-custom-version').value.trim() : select;
|
||||
const always = document.getElementById('cfg-nbu-always').checked;
|
||||
return { version, always };
|
||||
}
|
||||
|
||||
// NetBird client auto-update master default form
|
||||
document.getElementById('settings-nbu-master-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const { version, always } = _readNbuMasterForm();
|
||||
if (!version) {
|
||||
showSettingsAlert('danger', t('customer.nbuVersionRequired'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api('PUT', '/settings/system', {
|
||||
netbird_client_auto_update_version: version,
|
||||
netbird_client_auto_update_always: always,
|
||||
});
|
||||
showSettingsAlert('success', t('messages.imageSettingsSaved'));
|
||||
} catch (err) {
|
||||
showSettingsAlert('danger', t('errors.failed', { error: err.message }));
|
||||
}
|
||||
});
|
||||
|
||||
async function applyNetbirdUpdatesToAll() {
|
||||
const { version, always } = _readNbuMasterForm();
|
||||
if (!version) {
|
||||
showSettingsAlert('danger', t('customer.nbuVersionRequired'));
|
||||
return;
|
||||
}
|
||||
if (!confirm(t('customer.nbuConfirmApplyAll'))) return;
|
||||
|
||||
const btn = document.getElementById('btn-nbu-apply-all');
|
||||
const resultDiv = document.getElementById('nbu-apply-all-result');
|
||||
btn.disabled = true;
|
||||
resultDiv.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${t('common.loading')}`;
|
||||
try {
|
||||
const data = await api('POST', '/monitoring/netbird-updates/apply-all', { version, always });
|
||||
const rows = data.results.map(r => `<tr>
|
||||
<td>${esc(r.customer_name)}</td>
|
||||
<td>${r.success
|
||||
? '<span class="badge bg-success"><i class="bi bi-check-lg"></i> OK</span>'
|
||||
: '<span class="badge bg-danger"><i class="bi bi-x-lg"></i> Error</span>'}</td>
|
||||
<td class="small text-muted">${esc(r.error || '')}</td>
|
||||
</tr>`).join('');
|
||||
resultDiv.innerHTML = `<div class="alert alert-${data.updated === data.results.length ? 'success' : 'warning'}">
|
||||
<strong>${esc(data.message)}</strong>
|
||||
<table class="table table-sm mb-0 mt-2">
|
||||
<thead><tr><th>${t('monitoring.thName')}</th><th>${t('monitoring.thStatus')}</th><th></th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
} catch (err) {
|
||||
resultDiv.innerHTML = `<div class="alert alert-danger">${esc(err.message)}</div>`;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Test NPM connection
|
||||
async function testNpmConnection() {
|
||||
const spinner = document.getElementById('npm-test-spinner');
|
||||
|
||||
+21
-1
@@ -91,7 +91,27 @@
|
||||
"lastCheck": "Letzte Prüfung: {time}",
|
||||
"openDashboard": "Dashboard öffnen",
|
||||
"updateImages": "Images aktualisieren",
|
||||
"updateInProgress": "Container werden aktualisiert — bitte warten…"
|
||||
"updateInProgress": "Container werden aktualisiert — bitte warten…",
|
||||
"netbirdClientUpdates": "NetBird Client Auto-Updates",
|
||||
"nbuNoToken": "Kein API-Token für diesen Kunden hinterlegt. Bei Neu-Deployments wird das automatisch erfasst — für bestehende Kunden einmalig ein Personal Access Token im Kunden-Dashboard erstellen (Settings → Service Users) und hier einfügen.",
|
||||
"nbuTokenPlaceholder": "Personal Access Token einfügen…",
|
||||
"nbuSaveToken": "Prüfen & Speichern",
|
||||
"nbuTokenSaved": "Token gespeichert.",
|
||||
"nbuVersion": "Client-Version",
|
||||
"nbuDisabled": "Deaktiviert",
|
||||
"nbuLatest": "Neueste Version",
|
||||
"nbuCustom": "Bestimmte Version",
|
||||
"nbuForce": "Automatische Updates erzwingen",
|
||||
"nbuSave": "Speichern",
|
||||
"nbuSyncMaster": "Vom Standard übernehmen",
|
||||
"nbuSaved": "Einstellung übernommen.",
|
||||
"nbuSynced": "Standard-Einstellung übernommen.",
|
||||
"nbuVersionRequired": "Bitte eine Version angeben.",
|
||||
"nbuMasterTitle": "NetBird Client Auto-Updates (alle Kunden)",
|
||||
"nbuMasterHint": "Steuert die \"Automatische Updates\"-Einstellung im NetBird-Dashboard jedes Kunden (Settings → Clients). Hier den Standard festlegen und auf alle Kunden anwenden. Einzelne Kunden können weiterhin über ihre Detailseite abweichend eingestellt werden.",
|
||||
"nbuSaveDefault": "Standard speichern",
|
||||
"nbuApplyAll": "Auf alle Kunden anwenden",
|
||||
"nbuConfirmApplyAll": "Diese Update-Einstellung auf alle Kunden mit hinterlegtem API-Token anwenden?"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Systemeinstellungen",
|
||||
|
||||
+21
-1
@@ -91,7 +91,27 @@
|
||||
"lastCheck": "Last check: {time}",
|
||||
"openDashboard": "Open Dashboard",
|
||||
"updateImages": "Update Images",
|
||||
"updateInProgress": "Updating containers — please wait…"
|
||||
"updateInProgress": "Updating containers — please wait…",
|
||||
"netbirdClientUpdates": "NetBird Client Auto-Updates",
|
||||
"nbuNoToken": "No API token registered for this customer. New deployments capture one automatically — for existing customers, create a Personal Access Token once in their dashboard (Settings → Service Users) and paste it here.",
|
||||
"nbuTokenPlaceholder": "Paste Personal Access Token…",
|
||||
"nbuSaveToken": "Verify & Save",
|
||||
"nbuTokenSaved": "Token saved.",
|
||||
"nbuVersion": "Client version",
|
||||
"nbuDisabled": "Disabled",
|
||||
"nbuLatest": "Latest version",
|
||||
"nbuCustom": "Specific version",
|
||||
"nbuForce": "Force automatic updates",
|
||||
"nbuSave": "Save",
|
||||
"nbuSyncMaster": "Sync from default",
|
||||
"nbuSaved": "Setting applied.",
|
||||
"nbuSynced": "Default setting applied.",
|
||||
"nbuVersionRequired": "Please specify a version.",
|
||||
"nbuMasterTitle": "NetBird Client Auto-Updates (all customers)",
|
||||
"nbuMasterHint": "Controls the \"Automatic Updates\" setting inside every customer's own NetBird dashboard (Settings → Clients). Set the default here, then push it to all customers at once. Individual customers can still be overridden from their detail page.",
|
||||
"nbuSaveDefault": "Save Default",
|
||||
"nbuApplyAll": "Apply to All Customers",
|
||||
"nbuConfirmApplyAll": "Apply this update setting to every customer with a registered API token?"
|
||||
},
|
||||
"customerModal": {
|
||||
"newCustomer": "New Customer",
|
||||
|
||||
Reference in New Issue
Block a user