Files
itflow/cron/managed_instances.php
johnnyq 3b5c11c552 Add managed mode, so one ITFlow can manage others - Admin > Managed Instances adds an instance and gives a one-time connection code (a week) that the other ITFlow pastes into the new Admin > Managed By, after which it checks in every minute from the new Manager Check-in cron job with a health report (versions, update waiting and whether it has security fixes, cron, last backup, errors today, agents against the seat limit, clients, storage) and takes back what the manager sets; the manager lists each instance's version, health, seats and last check-in with a page per instance showing what needs a look, its requests and everything it reported, can update it by hand or automatically after a set number of days (security releases straight away, not retried for six hours after a failure), set its seat limit and white-label key (locked on the instance while set), make it read-only (look, export and download only, across agents, portal, guest links and the API) or suspended (a notice with the manager's message and contact, 503 for the API) with other cron jobs paused until it is active again, sign in to it as support through a signed one-time two-minute link into a two-hour session that takes no seat, can't open the vault and can't set passwords or make API keys, and remove it, while the new Managed Instances job notifies when one stops checking in and closes requests never answered; instances only call the manager, can disconnect unless config.php sets $config_manager_required, and the Update page and queue_update defer to the manager; database 3.5.8 adds managed_instances, managed_instance_commands, settings.config_manager_keys and settings.config_managed, the logic is in the new functions/managed.php with api/managed/enroll.php, api/managed/checkin.php, login_support.php and includes/managed_blocked.php, cronJobClaim() takes a paused reason that leaves a due job due, getAgentUserLimit() and getActiveAgentUserCount() honour the manager's limit and leave out the support user, the vault helpers read a missing session key as empty, the audit log knows the new types and actions, and the email parser no longer logs an App Logs error every minute while parsing is switched off; changelog updated
2026-11-05 15:22:41 -05:00

81 lines
4.4 KiB
PHP

<?php
/*
* ITFlow - Managed instances job
*
* For an ITFlow that manages others (Admin > Managed Instances): notices what the instances
* don't say themselves. An instance that has stopped checking in gets one notification, until
* it's back (the check-in clears it - managerCheckin()); a request it took and never answered,
* or an update that never said it finished, is closed as failed rather than left waiting.
*/
// Set working directory to the directory this cron script lives at.
chdir(dirname(__FILE__));
// Ensure we're running from command line
if (php_sapi_name() !== 'cli') {
die("This script must be run from the command line.\n");
}
// Prevent overlapping runs of this script
$cron_lock_script = __FILE__;
require_once "includes/cron_lock.php";
require_once "../config.php";
// Set Timezone
require_once "../includes/inc_set_timezone.php";
require_once "../functions.php";
$managed_instances_settings = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT config_enable_cron FROM settings WHERE company_id = 1"));
if (intval($managed_instances_settings['config_enable_cron'] ?? 0) === 0) {
cronJobStop("Cron: is not enabled\n");
}
// Files newer than the schema - the tables arrive with the migration
if (!settingsColumnExists($mysqli, 'config_manager_keys')) {
cronJobStop("The database update has not been applied yet\n");
}
$managed_instances_count = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM managed_instances
WHERE managed_instance_archived_at IS NULL AND managed_instance_secret_hash IS NOT NULL"))[0] ?? 0);
if ($managed_instances_count === 0) {
cronJobStop("No instances connected\n");
}
// Gone quiet - told once, until it checks in again
$managed_instances_quiet = mysqli_query($mysqli, "SELECT managed_instance_id, managed_instance_name, managed_instance_client_id, managed_instance_last_checkin_at FROM managed_instances
WHERE managed_instance_archived_at IS NULL AND managed_instance_secret_hash IS NOT NULL AND managed_instance_offline_notified_at IS NULL
AND (managed_instance_last_checkin_at IS NULL OR managed_instance_last_checkin_at < NOW() - INTERVAL " . MANAGED_OFFLINE_MINUTES . " MINUTE)");
while ($managed_instance = mysqli_fetch_assoc($managed_instances_quiet)) {
$managed_instance_id = intval($managed_instance['managed_instance_id']);
$managed_instance_last = $managed_instance['managed_instance_last_checkin_at'];
appNotify("Managed Instance", escapeSql($managed_instance['managed_instance_name'] . " has stopped checking in" . ($managed_instance_last ? " - last " . date('M j, g:i A', strtotime($managed_instance_last)) : '')),
"/admin/managed_instance.php?id=$managed_instance_id", intval($managed_instance['managed_instance_client_id']), $managed_instance_id);
mysqli_query($mysqli, "UPDATE managed_instances SET managed_instance_offline_notified_at = NOW() WHERE managed_instance_id = $managed_instance_id");
}
// A request it took and never answered
mysqli_query($mysqli, "UPDATE managed_instance_commands SET managed_command_status = 'failed', managed_command_done_at = NOW(),
managed_command_result = 'It took the request but never answered - see whether it is still checking in'
WHERE managed_command_status = 'sent' AND managed_command_sent_at < NOW() - INTERVAL 1 HOUR");
// An update that never said it finished
$managed_instances_stuck = mysqli_query($mysqli, "SELECT managed_command_id, managed_instance_id, managed_instance_name, managed_instance_client_id
FROM managed_instance_commands JOIN managed_instances ON managed_instance_id = managed_command_instance_id
WHERE managed_command = 'update_now' AND managed_command_status = 'running' AND managed_command_sent_at < NOW() - INTERVAL 2 HOUR");
while ($managed_instance = mysqli_fetch_assoc($managed_instances_stuck)) {
mysqli_query($mysqli, "UPDATE managed_instance_commands SET managed_command_status = 'failed', managed_command_done_at = NOW(),
managed_command_result = 'No word of the update finishing in two hours - look at its Maintenance > Update page'
WHERE managed_command_id = " . intval($managed_instance['managed_command_id']));
appNotify("Managed Instance", escapeSql("The update on " . $managed_instance['managed_instance_name'] . " hasn't finished in two hours"),
"/admin/managed_instance.php?id=" . intval($managed_instance['managed_instance_id']), intval($managed_instance['managed_instance_client_id']), intval($managed_instance['managed_instance_id']));
}