Merge wilm: gevent migration, endpoint telemetry phase 2, backup/restore
Brings in 98 commits from wilm (origin/wilm @ 811e6ca61a):
Major features:
- Move API to gevent workers for better concurrency
- Endpoint agent startup jitter (prevents thundering herd)
- Complete backup/restore with restic
- Endpoint telemetry phase 2 (disk health, mapped drives, user context)
- PC Detail UI rework with new cards and tabs
- Dell Command actions and Corporate Tools tab
- Agent version column in Fleet view
Conflict resolution:
- Backup/restore: Accepted wilm implementation (10 files)
- Agent startup jitter: Accepted wilm v1.12.0
- PC Types list fix: Accepted wilm version
- Endpoint plugin domain clamping: Kept ours (defensive)
- Test files: Accepted wilm versions for consistency
Testing required:
- Verify gevent handles concurrent requests
- Test backup/restore functionality
- Verify agent startup jitter timing
- Test endpoint telemetry collection
- Validate PC Detail UI changes
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -107,10 +107,9 @@ onMounted(() => loadData())
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
// listAll resolves to a plain array (fetchAllPages), not an axios
|
||||
// response. Reading .data.data threw and left the page empty.
|
||||
const response = await computersApi.types.listAll({ active: false })
|
||||
pcTypes.value = response || []
|
||||
// listAll() pages past the server's 100-row cap and returns the ROWS, not
|
||||
// an axios response - there is no .data to unwrap.
|
||||
pcTypes.value = await computersApi.types.listAll({ active: false })
|
||||
} catch (err) {
|
||||
console.error('Error loading PC types:', err)
|
||||
} finally {
|
||||
|
||||
@@ -1828,6 +1828,25 @@ def revoke_agentkey():
|
||||
# How long the agent claim poll is held open server-side before returning empty.
|
||||
# Kept below the agent HttpClient timeout and typical proxy idle timeouts so a
|
||||
# quiet fleet re-polls cleanly instead of tripping a read timeout.
|
||||
#
|
||||
# 25s is the value this endpoint is designed around; it was temporarily dropped
|
||||
# to 5s under the blocking gthread worker pool, where a held connection pinned
|
||||
# one of a worker's 8 OS threads. Measured then: 14 agents at hold=25 left a
|
||||
# single worker with 8 of 9 threads parked in timed sleep and UI requests hashed
|
||||
# to that worker waited 12-24s. Under gevent a held connection is a greenlet and
|
||||
# the per-worker ceiling is --worker-connections (2000), not 8 threads, so an
|
||||
# idle hold no longer costs anything a UI request needs.
|
||||
#
|
||||
# Restored to 25 because hold length sets the fleet's REQUEST RATE, not its
|
||||
# concurrency: the agent re-polls immediately on a 204, so hold=5 is 5x the
|
||||
# requests of hold=25, and TLS handshake CPU was the measured burst bottleneck
|
||||
# (devnotes/gevent-sizing-simulation.md). 25 also restores the margin against
|
||||
# the agent's MinIdlePollDuration=5s floor, which CommandWorker.IsIdlePollClose
|
||||
# uses to tell a normal hold-window close apart from a transient failure; at
|
||||
# hold=5 that margin was ~20ms.
|
||||
#
|
||||
# Constraint: gunicorn --timeout must stay above this or a healthy long-poll is
|
||||
# killed mid-hold. docker-compose.yml sets 60.
|
||||
_CLAIM_HOLD_SECONDS = 25
|
||||
_CLAIM_POLL_INTERVAL = 1.0
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@ _BASE_APPSETTINGS = {
|
||||
'apikey': 'REPLACE_WITH_COLLECTOR_KEY',
|
||||
'cadenceseconds': 900,
|
||||
'jitterseconds': 60,
|
||||
'startupjitterseconds': 30,
|
||||
'adoptonfirstrun': True,
|
||||
'utilitiespath': '',
|
||||
'logpath': '',
|
||||
|
||||
@@ -1123,12 +1123,18 @@ def set_asset_printer_assignment(asset_id: int):
|
||||
', '.join(str(assetid) for assetid in sorted(notprinters))))
|
||||
|
||||
typeids = _relationship_typeids(_USES_PRINTER, _DEFAULT_PRINTER)
|
||||
if not typeids[_USES_PRINTER] or not typeids[_DEFAULT_PRINTER]:
|
||||
missingtypes = [name for name in (_USES_PRINTER, _DEFAULT_PRINTER)
|
||||
if not typeids[name]]
|
||||
if missingtypes:
|
||||
# Seed data, not a migration. An un-seeded database cannot hold an
|
||||
# assignment, and saying so beats writing rows nothing can read.
|
||||
# assignment, and saying so beats writing rows nothing can read. Name
|
||||
# the types that are actually missing: a partially seeded table is the
|
||||
# common case, and "types are not seeded" sends someone looking for an
|
||||
# empty table they will not find.
|
||||
return error_response(
|
||||
ErrorCodes.INTERNAL_ERROR,
|
||||
'Relationship types are not seeded - run: flask seed reference-data',
|
||||
'Relationship type(s) not seeded: {0} - run: flask seed '
|
||||
'reference-data'.format(', '.join(missingtypes)),
|
||||
http_code=500)
|
||||
|
||||
_reconcile_edges(asset_id, typeids[_USES_PRINTER][0],
|
||||
@@ -1784,10 +1790,13 @@ def seed_assignment_from_observed(asset_id: int):
|
||||
details={'hostname': hostname, 'skipped': candidate['skipped']})
|
||||
|
||||
typeids = _relationship_typeids(_USES_PRINTER, _DEFAULT_PRINTER, _CONTROLS)
|
||||
if not typeids[_USES_PRINTER] or not typeids[_DEFAULT_PRINTER]:
|
||||
missingtypes = [name for name in (_USES_PRINTER, _DEFAULT_PRINTER)
|
||||
if not typeids[name]]
|
||||
if missingtypes:
|
||||
return error_response(
|
||||
ErrorCodes.INTERNAL_ERROR,
|
||||
'Relationship types are not seeded - run: flask seed reference-data',
|
||||
'Relationship type(s) not seeded: {0} - run: flask seed '
|
||||
'reference-data'.format(', '.join(missingtypes)),
|
||||
http_code=500)
|
||||
|
||||
warnings = []
|
||||
|
||||
Reference in New Issue
Block a user