Skip to content

issue#130: per-device-type post-login prompt/response handling - #199

Merged
TheWitness merged 14 commits into
developfrom
feature/issue-130-custom-login-prompts
Nov 13, 2026
Merged

TheWitness merged 14 commits into
developfrom
feature/issue-130-custom-login-prompts

Conversation

@TheWitness

Copy link
Copy Markdown
Member

Description

Third PR in the stacked chain (#133 → #117 → #130 → #109), built on top of the #117 branch. It adds a per-Device-Type Post-Login Prompts field so devices that interpose extra prompts between login and the CLI can be backed up.

  • A new multiline Post-Login Prompts field on the Device Type form, stored in a new loginprompts column on plugin_routerconfigs_devicetypes. Each line is a prompt|response pair.
  • Right after login and before the copy-to-TFTP command, PHPShellConnection::Download() calls a new ProcessLoginPrompts(): for each configured pair it waits briefly for the prompt to appear (matched as a case-insensitive regular expression, consistent with how the existing promptconfirm is matched) and then sends the response. A response of <CR> (or an empty response) sends just a carriage return, so "Press any key to continue" style prompts and <CR>/numeric stacked-switch selections can both be answered.
  • The parsing is a small, pure, unit-tested helper (plugin_routerconfigs_parse_login_prompts()); the transport method that drives the live shell lives in the allowlisted connection class.

Example configuration for the HPE Aruba stack in the issue:

Press any key|<CR>
Enter switch number to connect to or <CR>:|<CR>

Related Issue

#130

Motivation and Context

Some devices (e.g. an HPE Aruba switch that is a stack member) present prompts after SSH login — "Press any key to continue" and "Enter switch number to connect to or <CR>:" — that the existing Any-Key / Confirmation fields only handle after the copy-to-TFTP command, so the backup fails. This lets an operator declare those prompts and their responses per Device Type, answered at the right point in the session.

How Has This Been Tested?

Ran the plugin's Pest suite from a Cacti checkout with the plugin as a real plugins/routerconfigs directory (the stacked diff covers #133 + #117 + #130):

  • Full suite: pest --configuration phpunit.xml → 179 passed (1123 assertions). Added tests/Unit/RouterconfigsLoginPromptsTest.php covering empty/whitespace input, ordered trimmed parsing, <CR>/empty normalization, skipping of blank / separator-less / empty-prompt lines, CRLF/CR line endings, and first-separator-only splitting.
  • Patch coverage gate: php tests/bin/patch-coverage.php coverage/clover.xml origin/develop 100 → 100.00% (211/211 lines).
  • i18n gate: php tests/bin/check-i18n-pot.php origin/develop → OK (locales/po/cacti.pot regenerated; per-language .po/.mo left to Weblate).
  • PHP syntax: php -l clean on every changed file.
  • Manifest: php tests/bin/validate-manifest.php → matches.

The ProcessLoginPrompts() transport method needs a live SSH/Telnet shell and is not exercised by unit tests (its class is in the patch-coverage allowlist); the pure parser it depends on is fully unit-tested.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.

@TheWitness
TheWitness force-pushed the feature/issue-130-custom-login-prompts branch from 17f946f to b41ec24 Compare November 11, 2026 23:48
@TheWitness
TheWitness force-pushed the feature/issue-130-custom-login-prompts branch from b41ec24 to 813bf03 Compare November 11, 2026 23:57

@somethingwithproof somethingwithproof left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker: a prompt containing / never matches, and the response is sent anyway. ProcessLoginPrompts() builds the pattern as '/' . $entry['prompt'] . '/i' and suppresses the error. A prompt such as [y/n] or Select member (1/2) breaks that pattern, so preg_match() returns false on every try. After 5 attempts the loop sends the response anyway. The same fallthrough happens for any prompt that never appears, so a configured response like y or 2 can land on the CLI prompt and be run as a command.

  • Use a delimiter that can't clash with the prompt, or escape it.
  • Validate the patterns in save_devicetypes().
  • Skip DoCommand() when the prompt wasn't seen.

Suggestion: this overlaps the existing anykey device-type field, which already answers "press any key" prompts. The field description should say which one runs first, or whether both can be set together.

SonarCloud duplicated-lines failure (4.0%): the only new duplicated block is the 7-line loginprompts textarea definition in includes/arrays.php. It has the same shape as the other field arrays in that file, and the PR adds so little code that those 7 lines alone come to 4%. There are two ways to clear it:

  • Build that field from a shared definition. The device-type form already repeats the same textarea keys, so a small plugin_routerconfigs_textarea_field($name, $label, $description) helper could cover them.
  • Exclude the declarative form arrays from copy-paste detection with sonar.cpd.exclusions=includes/arrays.php, since repeated array shapes there aren't real duplicated logic.

…le on change

Monthly without a day of month used the day of the current run, so a Jan 31
run asked for "February 31", which strtotime() turned into March 3, and the
device drifted from then on. A Monthly schedule saved, or bulk-set, without
a day now stores the day it was set up on (Last, 32, after the 28th so short
months still run), and the calculator falls back to the 1st rather than the
run day. Days a month does not have (February 30, April 31) are skipped, as
RFC 5545 section 3.3.10 requires, instead of rolling into the next month.

The replica is now used on every Cacti version. Core's
api_scheduler_calculate_next_start() rolls nonexistent dates over, so
delegating to it on 1.3+ would give the same device different backup dates
on 1.2 and 1.3.

Bulk edit validated nothing: sched_type, account and devicetype were cast
to int, so an unknown sched_type resolved to 0 and the device ran on every
scheduled poll. plugin_routerconfigs_bulk_edit_validate() now accepts only
the offered Schedule Types, Rerun Every 1-31, a known Connection Type, and
existing accounts and device types; anything else raises an error and
nothing is changed. save_devices() checks Schedule Type and Rerun Every the
same way.

Changing the schedule now recomputes nextbackup on the device form and in
bulk edit, so a device moved from Daily to Monthly no longer runs once
more on the old schedule. Manual stores 0, which the download query
excludes. A device that has never been scheduled keeps NULL so its first
backup still runs next. Bulk edit reads the selected rows once and groups
devices with identical results into one UPDATE ... WHERE id IN (...).

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…sue-117-schedule-days-datedir

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Now that nonexistent dates are skipped, a Monthly schedule on February 29
alone has no date in 2026 or 2027. The calculator tried only this year and
next, so it returned 0 and the device became due on every scheduled run.
It now searches 8 years, the longest gap between leap years (2096 to 2104),
and still returns false for a day that never exists, such as February 30.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…sue-117-schedule-days-datedir

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…p missing days

Bulk edit offered every Schedule Type, including Monthly on Day, but never
sets Months, Weeks of Month or Days of Week. A device bulk-set to Monthly on
Day had nothing to resolve, so nextbackup became 0 and it was backed up on
every scheduled run. Monthly on Day is now left out of the bulk list, a bulk
Monthly gets every month when none is stored (and a fixed day of month, as
on the device form), and each device's resulting row must pass
plugin_routerconfigs_schedule_validate(). If any device fails, nothing is
changed and the failing device ids are reported.

plugin_routerconfigs_schedule_validate() also rejects a monthly selection
that never occurs (only February 30 and 31, say), which would otherwise
resolve to 0. Days a month lacks are skipped rather than rolled over, so a
31st selected for every month no longer adds May 1, July 1, October 1 and
December 1 runs; tests cover that through the multi-select save path.

Weekly with Days of Week selected runs on those days every week and ignored
Rerun Every, while the field said "every X weeks". Rerun Every is now hidden
once Days of Week are selected, and both descriptions say how they combine.

When validation fails the redirect still returns to a fresh edit form. The
plugin does not use Cacti's form_input_validate() field retention anywhere,
so keeping the entered values would mean adopting it for the whole form;
that is left for a separate change.

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

ProcessLoginPrompts() built each pattern as '/' . $prompt . '/i' and
suppressed the error, so a prompt containing '/' (such as '[y/n]' or
'Select member (1/2)') never compiled and never matched. After 5 reads the
loop sent the response anyway, and the same happened for any prompt that
did not appear, so a configured 'y' or '2' could land on the CLI prompt and
run as a command.

plugin_routerconfigs_regex() now compiles an operator-supplied pattern:
it escapes an unescaped '/', keeps an already escaped '\/', and returns
null for a pattern that does not compile, without emitting a warning. A
response is only sent once its prompt has been seen; otherwise a WARNING is
logged and nothing is sent. Download() then carries on, and EnsureEnabled()
fails the backup if the device is still waiting at an unanswered prompt.
save_devicetypes() rejects prompts that are not valid regular expressions.

The field description now says that a response is only sent after its
prompt is seen, that a prompt cannot contain '|', and that Post-Login
Prompts are answered before the Any Key prompt, which is handled later
while the device is being enabled.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…ssue-130-custom-login-prompts

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

@somethingwithproof somethingwithproof left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked against the previous review.

  • Prompt containing /: fixed. plugin_routerconfigs_regex() handles / and \/, save_devicetypes() rejects patterns that don't compile, and ProcessLoginPrompts() no longer sends a response when its prompt wasn't seen.
  • Overlap with anykey: fixed, the field description now says which runs first.
  • SonarCloud duplication: the gate passes now.

Suggestion: classes/PHPShellConnection.php:338: chained prompts. $response starts empty for each entry and DoCommand() at line 359 consumes the device's reply. If the second prompt is printed in reply to the first response (for example "Press any key" then "Select member"), it has already been read and discarded. The second entry then logs "not seen" and is skipped, where the old code sent it regardless. Check the previous DoCommand() result against the next prompt before reading again.

Suggestion: a prompt typed as [y/n] is a valid character class, so it passes the save-time check and then matches any output containing y, n or /. The response goes out on the first read. Mention in the field description that brackets must be escaped (\[y/n\]).

No blockers. Only SonarCloud runs on this stacked PR (it passes); the PHP integration jobs run on #197.

Each prompt started from an empty buffer and the reply to the previous
response was thrown away, so a chained prompt (Press any key, then
Select member) had already been read and was never seen: it was logged
as missing and skipped. The reply to each response, and any output
read while waiting for a prompt that never came, is now checked against
the next prompt before reading more.

The field description also says that brackets and other special
characters must be escaped, since an unescaped [y/n] is a character
class that matches almost any output.

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

Copy link
Copy Markdown
Member

Pushed for the review and the follow-up suggestions:

  • 49fa5ce: prompts are compiled by plugin_routerconfigs_regex(). It escapes a bare / (keeping \/), validates the pattern without warnings, and caches the result. save_devicetypes() rejects prompts that don't compile. ProcessLoginPrompts() sends a response only after its prompt is seen; otherwise it logs a warning and moves on. The description says which runs first, the login prompts or Any Key.
  • da89ac1: merge of issue#117: per-day/month schedule selection and dated backup subdirectories #198, with the POT regenerated.
  • f85494c: chained prompts work. The reply to each response, and any output read while waiting for a prompt that never came, is checked against the next prompt before reading more. The description now says that brackets must be escaped (\[y/n\]).

Tests: RouterconfigsLoginPromptsTest, using a stub shell, covers / in prompts, not-seen, invalid patterns and both chained-prompt cases. The [y/n] and not-seen tests sent the response on the previous code, and the two chained tests fail on da89ac1. 224 passed locally. SonarCloud passes on f85494c; the duplicated-lines failure from the first review cleared once the change grew.

To revisit:

  • A prompt that isn't seen continues, so the enable step then fails the backup if the device is stuck, rather than failing immediately.
  • The older promptuser/promptpass/promptconfirm/anykey fields still build '/' . $pattern . '/i' in PHPConnection/PHPShellConnection. They could move to the same helper in a separate change.

Compiles more distinct patterns than the 256-entry cache holds, so the reset branch runs; clears the 100% changed-line coverage gate.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Copilot AI balanced review requested due to automatic review settings November 12, 2026 02:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Post-login menu handling can discard prompts, is unreachable for Telnet devices stopped at a menu, and sends LF rather than the required carriage return on Linux, preventing login completion.

3 open issues
What changed in this PR

Adds per-device-type post-login prompt handling to the Cacti routerconfigs plugin, supporting devices such as HPE Aruba stacks that require extra input before configuration backups.

Changes:

  • Adds a Post-Login Prompts form field, database column, and regular-expression validation.
  • Processes ordered prompt/response pairs before shell backup commands.
  • Adds parser and scripted-shell tests, updates translations and release notes, and bumps the version to 1.11.
File Description
tests/​Unit/​RouterconfigsLoginPromptsTest.php Tests parsing, regex validation, and scripted prompt handling.
router-devtypes.php Validates and saves configured login prompts.
locales/​po/​cacti.pot Adds field-help and validation-message translations.
INFO Bumps the plugin version to 1.11.
includes/​functions.php Adds prompt parsing and regex validation helpers.
includes/​database.php Adds the loginprompts device-type column.
includes/​arrays.php Defines the Post-Login Prompts form field and help text.
classes/​PHPShellConnection.php Adds prompt processing before shell backup commands.
CHANGELOG.md Documents the feature for issue #130.

Review effort: Balanced

Comment thread classes/PHPShellConnection.php Resolved
Comment thread classes/PHPShellConnection.php Resolved
Comment thread classes/PHPShellConnection.php Resolved

@somethingwithproof somethingwithproof left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chained prompts are handled now: the reply to one response, and any output read while waiting for a prompt that never came, is checked against the next prompt before reading again. The two new tests cover both paths, and the cache-reset test clears the patch-coverage gate.

The earlier suggestion about [y/n] being a character class still stands as a field-description note. No blockers, and CI is green on 8.2-8.6.

xmacan
xmacan previously approved these changes Nov 12, 2026
…ith CR

PHPTelnet::Connect() disconnected when the password reply was a menu rather
than a CLI prompt, so Download() never got to answer the configured prompts.
Connect() now answers them there, passing the reply already read, and each
connection answers them at most once.

Responses went through DoCommand(), which ends the line with PHP_EOL (LF on
Linux) and trims the reply to its last line, dropping a prompt printed with
it. SendResponse() writes the answer followed by CR and returns the reply
untrimmed. A socket-pair test drives the real transport.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
@somethingwithproof
somethingwithproof dismissed stale reviews from xmacan and themself via 77c3526 November 12, 2026 07:59
xmacan
xmacan previously approved these changes Nov 13, 2026
Base automatically changed from feature/issue-117-schedule-days-datedir to develop November 13, 2026 19:01
Resolve conflicts after #198 (issue#117 + issue#133) landed on develop:

- scheduler.php, RouterconfigsSchedulerTest.php: take develop's version;
  it is a superset of this branch's issue#117-era copy (adds the DST-aware
  strtotime Daily/Weekly math, plugin_routerconfigs_schedule_fields_valid()
  and its test). issue#130 does not touch scheduling.
- router-devices.php: keep develop's save_devices() flow that validates the
  multi-select schedule inputs (schedule_fields_valid) before saving; the
  issue#130 post-login-prompt handling merged cleanly alongside it.
- database.php: keep develop's day_of_month varchar(128) (a full Days of
  Month selection is an 86-char CSV); the issue#130 device-type columns
  merged cleanly.
- INFO: keep this branch's version bump to 1.11.
- CHANGELOG: union - issue#130 entry plus develop's issue#117/#133 entries
  (including the DST line).
- cacti.pot: regenerated from the merged source (issue#130 + issue#117
  strings); per-language .po files taken from develop unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@TheWitness
TheWitness dismissed stale reviews from xmacan and somethingwithproof via a65997d November 13, 2026 19:09
@sonarqubecloud

Copy link
Copy Markdown

@TheWitness
TheWitness merged commit 89ad1d6 into develop Nov 13, 2026
11 of 12 checks passed
@TheWitness
TheWitness deleted the feature/issue-130-custom-login-prompts branch November 13, 2026 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants