Files
cacti/tests/e2e/installer_failure_paths.php
Thomas Vincent 9899fdf221 fix(installer): allow forced replays after LDAP migration (#8317)
* fix(installer): preserve forced replay on already upgraded schemas

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* docs(changelog): record forced upgrade replay repair

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-11 18:57:46 -04:00

188 lines
12 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
*/
// This fixture mutates schema and migration files in a disposable Docker copy.
if (getenv('CACTI_INSTALLER_FAILURE_FIXTURE') !== '1') {
fwrite(STDERR, "Run through installer_resilience_docker.sh only.\n");
exit(1);
}
require __DIR__ . '/../../include/cli_check.php';
require_once CACTI_PATH_INSTALL . '/functions.php';
require_once CACTI_PATH_LIBRARY . '/installer.php';
class InstallerFailureStatement extends PDOStatement {
public static string $failQuery = '';
public function execute(?array $params = null) : bool {
$query = implode(' ', preg_split('/\s+/', trim($this->queryString)) ?: []);
if (self::$failQuery !== '' && str_starts_with($query, self::$failQuery)) {
throw new PDOException('Injected installer fixture read failure', 1142);
}
return parent::execute($params);
}
}
function installer_fixture_check(bool $condition, string $message) : void {
if (!$condition) {
throw new RuntimeException($message);
}
}
$scenario = $_SERVER['argv'][1] ?? '';
$key = "$database_hostname:$database_port:$database_default";
$pdo = $database_sessions[$key];
$version = db_fetch_cell('SELECT cacti FROM version');
$backups = [];
try {
if (in_array($scenario, ['domain-read', 'ldap-read', 'count-read'], true)) {
require_once CACTI_PATH_INSTALL . '/upgrades/1_3_0.php';
$pdo->exec('CREATE TABLE user_domains (domain_id INT PRIMARY KEY, domain_name VARCHAR(64), type INT, enabled VARCHAR(2), debug VARCHAR(2), defdomain INT, user_id INT)');
$pdo->exec("INSERT INTO user_domains VALUES (987, 'Installer fixture', 1, 'on', '', 0, 0)");
$pdo->exec('CREATE TABLE user_domains_ldap (domain_id INT PRIMARY KEY)');
// LDAP reads fail before accessing the deliberately minimal fixture row.
if ($scenario === 'ldap-read') {
$pdo->exec('INSERT INTO user_domains_ldap VALUES (987)');
}
$pdo->setAttribute(PDO::ATTR_STATEMENT_CLASS, [InstallerFailureStatement::class]);
InstallerFailureStatement::$failQuery = match ($scenario) {
'domain-read' => 'SELECT * FROM user_domains',
'ldap-read' => 'SELECT * FROM user_domains_ldap',
'count-read' => 'SELECT COUNT(*) FROM login_providers',
};
$database_upgrade_status = [];
$cacti_upgrade_version = '1.3.0';
login_providers_convert_1_3_0();
InstallerFailureStatement::$failQuery = '';
installer_fixture_check(count($pdo->query("SHOW TABLES LIKE 'user_domains%'")->fetchAll()) === 2, 'Failed read destroyed legacy authentication tables');
installer_fixture_check(in_array(DB_STATUS_ERROR, array_column($database_upgrade_status['1.3.0'] ?? [], 'status'), true), 'Failed read was not recorded in migration status');
installer_fixture_check((int) $pdo->query('SELECT COUNT(*) FROM user_domains')->fetchColumn() === 1, 'Legacy authentication row was lost');
} elseif ($scenario === 'forced-upgrade-replay') {
// Probe live metadata after each child process repairs the schema.
define('IN_CACTI_INSTALL', 1);
installer_fixture_check(!db_table_exists('user_domains') && !db_table_exists('user_domains_ldap'), 'Fixture is not an already upgraded schema');
db_execute_prepared('UPDATE version SET cacti = ?', [CACTI_VERSION_FULL]);
installer_fixture_check(db_execute_prepared('INSERT INTO login_providers (id, name, type, parameters) VALUES (?, ?, ?, ?)',
[9876, 'Forced replay fixture', 3, '{"fixture":"preserve"}']), 'Could not seed the migrated provider fixture');
installer_fixture_check(db_execute_prepared('INSERT INTO user_auth (id, username, realm) VALUES (?, ?, ?)',
[9876, 'forced-replay-fixture', 10876]), 'Could not seed the provider realm fixture');
installer_fixture_check(db_execute_prepared('INSERT INTO host_template (id, hash, name) VALUES (?, ?, ?)',
[9876, '0123456789abcdef0123456789abcdef', 'Forced replay template fixture']), 'Could not seed the host template fixture');
installer_fixture_check(db_execute_prepared('INSERT INTO host (id, host_template_id, description, hostname, disabled) VALUES (?, ?, ?, ?, ?)',
[9876, 9876, 'Forced replay host fixture', '127.0.0.1', 'on']), 'Could not seed the disabled host fixture');
$providers = db_fetch_assoc('SELECT * FROM login_providers ORDER BY id');
$users = db_fetch_assoc('SELECT id, username, realm FROM user_auth ORDER BY id');
$hosts = db_fetch_cell('SELECT COUNT(*) FROM host');
$templates = db_fetch_cell('SELECT COUNT(*) FROM host_template');
installer_fixture_check((int) $hosts > 0 && (int) $templates > 0, 'Monitoring fixture is not populated');
foreach (['1.2.31', '1.2.31', '1.2.28', '1.2.22'] as $forced_version) {
// A successful replay must still repair current schema changes.
db_execute('ALTER TABLE host DROP COLUMN snmp_retries');
$output = [];
exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(CACTI_PATH_CLI . '/upgrade_database.php') . ' --forcever=' . escapeshellarg($forced_version) . ' 2>&1', $output, $status);
installer_fixture_check($status === 0, 'Forced replay failed: ' . implode(PHP_EOL, $output));
installer_fixture_check(db_column_exists('host', 'snmp_retries', false), 'Forced replay skipped a required schema repair');
installer_fixture_check(db_fetch_cell('SELECT cacti FROM version') === CACTI_VERSION_FULL, 'Replay did not persist the current source version');
installer_fixture_check(db_fetch_assoc('SELECT * FROM login_providers ORDER BY id') === $providers, 'Replay changed migrated providers');
installer_fixture_check(db_fetch_assoc('SELECT id, username, realm FROM user_auth ORDER BY id') === $users, 'Replay changed authentication realms');
installer_fixture_check(db_fetch_cell('SELECT COUNT(*) FROM host') === $hosts && db_fetch_cell('SELECT COUNT(*) FROM host_template') === $templates, 'Replay lost populated monitoring data');
installer_fixture_check(!db_table_exists('user_domains') && !db_table_exists('user_domains_ldap'), 'Replay recreated obsolete authentication tables');
installer_fixture_check((int) db_fetch_cell("SELECT COUNT(*) FROM processes WHERE tasktype = 'install' AND taskname = 'master'") === 0, 'Replay retained the installer lock');
}
} elseif ($scenario === 'forced-cli-failure') {
$path = CACTI_PATH_INSTALL . '/upgrades/1_2_30.php';
$backups[$path] = is_file($path) ? file_get_contents($path) : false;
file_put_contents($path, '<?php /* Fixture: missing migration function after a skipped version. */');
db_execute_prepared('UPDATE version SET cacti = ?', [CACTI_VERSION_FULL]);
$output = [];
exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(CACTI_PATH_CLI . '/upgrade_database.php') . ' --forcever=1.2.28 2>&1', $output, $status);
installer_fixture_check($status !== 0 && str_contains(implode(PHP_EOL, $output), 'not found'), 'Forced replay swallowed a genuine migration failure');
installer_fixture_check(db_fetch_cell('SELECT cacti FROM version') === CACTI_VERSION_FULL, 'Failed replay downgraded the installed version');
installer_fixture_check((int) db_fetch_cell("SELECT COUNT(*) FROM processes WHERE tasktype = 'install' AND taskname = 'master'") === 0, 'Failed replay retained the installer lock');
} elseif (in_array($scenario, ['upgrade-warning', 'cli-failure'], true)) {
foreach (['1_2_29', '1_2_30'] as $target) {
$path = CACTI_PATH_INSTALL . '/upgrades/' . $target . '.php';
$backups[$path] = is_file($path) ? file_get_contents($path) : false;
}
file_put_contents(CACTI_PATH_INSTALL . '/upgrades/1_2_29.php', '<?php /* Fixture: missing migration function. */');
file_put_contents(CACTI_PATH_INSTALL . '/upgrades/1_2_30.php', '<?php function upgrade_to_1_2_30(): void { db_install_execute("SELECT 1"); }');
db_execute("UPDATE version SET cacti = '1.2.28'");
if ($scenario === 'upgrade-warning') {
$cacti_version_codes = array_intersect_key($cacti_version_codes, array_flip(['1.2.29', '1.2.30']));
$reflection = new ReflectionClass(Installer::class);
$installer = $reflection->newInstanceWithoutConstructor();
$reflection->getProperty('old_cacti_version')->setValue($installer, '1.2.28');
$error = $reflection->getMethod('upgradeDatabase')->invoke($installer);
installer_fixture_check($error !== '', 'Incomplete migration reported success');
} else {
$output = [];
exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(CACTI_PATH_CLI . '/upgrade_database.php') . ' --forcever=1.2.28 2>&1', $output, $status);
installer_fixture_check($status !== 0, 'CLI migration failure returned success');
installer_fixture_check(str_contains(implode(PHP_EOL, $output), 'not found'), 'CLI fixture did not exercise the missing migration');
}
installer_fixture_check(db_fetch_cell('SELECT cacti FROM version') === '1.2.28', 'Incomplete migration advanced the recorded version');
} elseif ($scenario === 'composer-failure') {
$composer = tempnam(sys_get_temp_dir(), 'installer-composer-');
installer_fixture_check($composer !== false, 'Could not create Composer fixture');
file_put_contents($composer, '#!' . PHP_BINARY . PHP_EOL . '<?php if (in_array("--dry-run", $argv, true)) { echo "Package operations: 1 install, 0 updates, 0 removals\n"; exit(0); } echo "download failed\n"; exit(7);');
chmod($composer, 0700);
set_config_option('path_composer', $composer);
$logPath = read_config_option('path_cactilog');
if ($logPath === '') {
$logPath = CACTI_PATH_LOG . '/cacti.log';
}
$offset = is_file($logPath) ? filesize($logPath) : 0;
$reflection = new ReflectionClass(Installer::class);
$installer = $reflection->newInstanceWithoutConstructor();
$reflection->getMethod('refreshVendorDependencies')->invoke($installer);
$log = substr(file_get_contents($logPath), $offset);
installer_fixture_check(str_contains($log, 'Composer refresh did not complete'), 'Failed Composer command did not log a warning');
installer_fixture_check(!str_contains($log, 'Composer dependencies refreshed.'), 'Failed Composer command reported success');
unlink($composer);
} elseif ($scenario === 'installer-lock') {
installer_fixture_check(register_process_start('install', 'master', 0, 86400), 'Could not register lock fixture');
set_config_option('install_eula', 'lock-owner-fixture');
set_config_option('log_install_json', '3');
try {
$output = [];
exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(CACTI_PATH_CLI . '/upgrade_database.php') . ' --forcever=1.2.28 2>&1', $output, $status);
installer_fixture_check($status !== 0 && str_contains(implode(PHP_EOL, $output), 'already in progress'), 'CLI upgrader ignored an active installer');
$arguments = ['--accept-eula', '--install', '--force', '--debug=json:5', '--path=php_binary:' . PHP_BINARY, '--path=rrdtool:/usr/bin/rrdtool', '--path=snmpwalk:/usr/bin/snmpwalk', '--path=snmpget:/usr/bin/snmpget', '--path=snmpbulkwalk:/usr/bin/snmpbulkwalk', '--path=snmpgetnext:/usr/bin/snmpgetnext', '--path=fping:/usr/bin/fping'];
$output = [];
exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(CACTI_PATH_CLI . '/install_cacti.php') . ' ' . implode(' ', array_map('escapeshellarg', $arguments)) . ' 2>&1', $output, $status);
installer_fixture_check($status !== 0 && str_contains(implode(PHP_EOL, $output), 'already in progress'), 'CLI installer ignored an active installer');
installer_fixture_check(db_fetch_cell("SELECT value FROM settings WHERE name = 'install_eula'") === 'lock-owner-fixture', 'Blocked CLI installer overwrote the active installer settings');
installer_fixture_check(db_fetch_cell("SELECT value FROM settings WHERE name = 'log_install_json'") === '3', 'Blocked CLI installer overwrote the active installer logging settings');
} finally {
unregister_process('install', 'master', 0);
}
} else {
throw new RuntimeException('Unknown fixture scenario');
}
print "PASS: $scenario" . PHP_EOL;
} finally {
InstallerFailureStatement::$failQuery = '';
foreach ($backups as $path => $contents) {
if ($contents === false) {
unlink($path);
} else {
file_put_contents($path, $contents);
}
}
db_execute_prepared('UPDATE version SET cacti = ?', [$version]);
}