Skip to content

feat(baremetal): say what a reinstall is doing while it does it - #253

Draft
Denis-hamon wants to merge 16 commits into
ovh:feat/baremetal-install-discoveryfrom
Denis-hamon:feat/baremetal-install-progress
Draft

feat(baremetal): say what a reinstall is doing while it does it#253
Denis-hamon wants to merge 16 commits into
ovh:feat/baremetal-install-discoveryfrom
Denis-hamon:feat/baremetal-install-progress

Conversation

@Denis-hamon

Copy link
Copy Markdown
Contributor

Stacked on #250. Only the last commit belongs to this pull request.

A reinstall said nothing for twenty minutes

reinstall --wait followed the task, and the task API answers whether the work
is finished and nothing else. So this was the whole of it, repeated every
thirty seconds:

Still waiting for task 559351769 to complete (status=doing)…

install/status has the missing half — one entry per step, with the running
one marked — and the CLI used it nowhere.

What it does now

ovhcloud baremetal install-status <server>     # read it, from anywhere
ovhcloud baremetal reinstall <server> --wait   # say which step, while waiting

Measured on a real reinstall of an ADVANCE-1 (EPYC 4245P, rbx8) — 11 min 05 s
end to end, exit 0
:

13:17:47 step 5/21: Preparing disks for new Partitioning
13:18:18 step 10/21: Applying Partitioning
13:18:49 step 13/21: Downloading OS image
13:19:19 step 16/21: Checking Partitioning
13:19:50 step 19/21: Waiting for services to be up

and install-status, against that same running installation:

**step 5 of 21 — Preparing disks for new Partitioning**

  # | Status | Step
  1 | done   | Checking BIOS version
  …
  5 | doing  | Preparing disks for new Partitioning
  6 | todo   | Cleaning Partitioning
  …
 21 | todo   | Sending end of installation mail

Two things the API does that the code has to know about

Outside an installation the route answers 404, with "Server is not being
installed or reinstalled at the moment"
— verified on three servers. That is
an answer, not a breakdown, so it is reported as one.

It is also why this endpoint cannot carry the verdict of a --wait: it says
the same thing before an installation starts and after it ends. The task stays
the authority on whether the work is done; install/status only says where it
has got to, and a progress read that fails is an empty line rather than a
failed command.

elapsedTime is not the age of the installation. Sampled every 30 s through
one real run:

time elapsedTime step
13:17:44 +42 5
13:18:14 +72 10
13:18:44 +102 13
13:19:15 +133 16
13:19:45 -106 19
13:20:15 -76 19
13:21:16 -15 19

It counts up at exactly one per second and never resets between steps, but its
origin is rebased at every machine restart — one discontinuity of 269
seconds at the final reboot, and an earlier -1935 during the hardware reboot.
It was reporting 1m47s while the install had been running for six minutes.

So --wait times itself from the request it sent, and install-status
attributes the figure to the API (· API reports 1m47s elapsed) rather than
asserting it as a duration measurement has shown it is not.

Verification

go build, go vet, make wasm, go test ./..., docgen — all green.

11 tests. 5 sabotages, 5 red, each on the test meant to catch it:

sabotage test that fell
the computed note is never printed PrintsTheNoteInsteadOfRepeatingItself
the idle 404 becomes an error again TestMockSuite
a negative elapsedTime is rendered as a duration RefusesToInventANegativeDuration
the running step becomes the first step CurrentStepIsTheOneBeingWorkedOn + IsAbsentWhenNothingIsRunning
a failed step's reason is dropped FailedStepCarriesItsReason

The empty-note path is covered too: a progress read that fails must leave the
wait saying what it always said, not fail it.

One thing found while running this, unrelated to the change

On main, reinstall --wait ends by calling GetBaremetalAuthenticationSecrets,
which prints the new SSH password in cleartext, unconditionally. Anyone
redirecting that output to a build log has the root password in it.

#233 already fixes this — it replaces that call with a masked
fetchAuthenticationSecrets behind --reveal. Verified that #233 and this
change merge cleanly in baremetal.go; the only conflicts are the two doc
pages and the cobra wiring where both add commands side by side.

Denis-hamon and others added 3 commits August 18, 2026 17:02
…to change

`vps service-info edit myvps --renew-period 12` used to send this:

    {"renew":{"automatic":false,"deleteAtExpiration":false,"forced":false,
              "manualPayment":false,"period":12}}

The renewal settings are booleans bound to a shared struct carrying no
`omitempty`, so every one of them was marshalled at its zero value and won the
merge against the fetched resource. Changing the renewal period therefore also
switched automatic renewal off — on a service that had been renewing itself for
years, without a word in the output saying so.

Webhosting already built its payload from `cmd.Flags().Changed`, and did not
have the defect. This promotes that builder into `common`, so `vps` gets it too
and the next `service-info edit` cannot reintroduce the bug by reaching for the
struct.

Reading `Changed` rather than the values keeps `--renew-automatic=false`
working: pflag records a flag as changed whatever value it was given, so an
explicit false is still sent while an absent flag stays absent. Both cases are
covered by a test, and each test was checked against the failure it exists to
catch.

The shared mutable `ServiceInfoSpec` goes away with the last thing that read
it, and the five flag registrations repeated across four commands become one
call, which also settles the two spellings of the period's help text.

Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
…ayer

Declaring cobra flags is the command layer's job, and internal/services/common
was the only service package doing it — the shared flag helpers all live in
internal/cmd. Raised in review of this PR.

The registration moves; the table does not. Both halves need the flag name —
one to declare it, the other to read whether the operator set it — and that
name is the only thing tying them together. Splitting it into two copies would
mean a rename could touch one side and leave the other silently no longer
sending a setting, which is the exact failure this PR exists to fix. So
common.ServiceInfoRenewFlags becomes the exported description, internal/cmd
registers from it, and the payload builder keeps reading it.

Checked by renaming an entry in that table and watching the service-info tests
fall: the two halves still move together.

Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`reinstall --wait` followed the task, and the task API answers whether the work
is finished and nothing else. So a reinstall that runs for twenty minutes
printed the same sentence sixty times:

    Still waiting for task 559351769 to complete (status=doing)…

install/status has the missing half — one entry per step, with the running one
marked — and the CLI used it nowhere. It does now, in two places:

  - `baremetal install-status <server>` reads it, for somebody who started the
    installation in another terminal, or who answered the confirmation, walked
    away, and wants to know whether to keep waiting.
  - `reinstall --wait` prints the step it is on instead of repeating itself.

Measured against a real reinstall of an ADVANCE-1:

    step 5/21: Preparing disks for new Partitioning
    step 10/21: Applying Partitioning
    step 13/21: Downloading OS image
    step 16/21: Checking Partitioning

Two things the API does that the code has to know about.

Outside an installation the route answers 404 with "Server is not being
installed or reinstalled at the moment". That is an answer, not a breakdown,
and it is also why this endpoint cannot carry the verdict of a --wait: it says
the same thing before an installation starts and after it ends. The task stays
the authority on whether the work is done; install/status only says where it
has got to, and a progress read that fails is an empty line rather than a
failed command.

And elapsedTime is not the age of the installation. Sampled through one real
run: -1935 during the hardware reboot, +42 four minutes later, -87 again after
the final reboot, counting up at exactly one per second in each stretch and
never resetting between steps. It is a continuous counter whose origin is
rebased every time the machine restarts. So --wait times itself from the
request it sent, and the read command attributes the figure to the API instead
of asserting it — it was reporting 1m47s while the install had been running
for six minutes.

Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Denis-hamon and others added 3 commits August 20, 2026 16:14
Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
The first sweep replaced only the exact spelling. A test that checks a name can
be copied back without reproducing its case carried the uppercase form of a
real hostname, which therefore survived — and, once its neighbour was
replaced, made that test fail. A real value does not stop being one because one
of its spellings was removed.

Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
@Denis-hamon
Denis-hamon changed the base branch from main to feat/baremetal-install-discovery August 20, 2026 15:19
Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>

# Conflicts:
#	internal/cmd/baremetal_install_test.go
Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
@Denis-hamon

Copy link
Copy Markdown
Contributor Author

Human review — Does not work

C1 · install-status: is that the right name?
Dimension: Naming

Expected: The name is findable and does not get confused with reinstall.

🛑 failed to fetch the installation status of ns0000001.ip-203-0-113.eu: OVHcloud API error (status code 401): Client::Unauthorized: "You must login first" (X-OVH-Query-Id: EU.ext-4.6a89762d.1101808.07340469ae1ad3e9577d931e9921a0d9)
exit 1

— Denis (Product Manager), through CLI sandbox review page

@Denis-hamon

Copy link
Copy Markdown
Contributor Author

Followed up on today's "Does not work" finding (C1, install-status returning 401 "You must login first").

Same cause as the identical 401 reported today on #248: the review sandbox's OVH_ACCESS_TOKEN had died at review time (26.6h old — no exp claim, but a previous one lasted ~27h) — the same 401 was reproducible on the sandbox's own account get, unrelated to this command.

Token refreshed. Rebuilt this branch and replayed live against the test/integration account:

$ ovhcloud baremetal install-status ns3118333.ip-51-68-100.eu
ns3118333.ip-51-68-100.eu is not being installed or reinstalled at the moment.

Works as intended. No code change needed here.

ovh#243 moved the Cobra registration of the renewal flags out of
internal/services/common and into the command layer, and exported the
descriptor table so both halves keep reading one list. That commit landed after
this branch had already taken its parent, so this branch still carried the
version where a service package declares flags — and being the more recent side
of the merge, it would have won and quietly undone the refactor.

Merged rather than rebased: nothing is rewritten, so the review threads on this
PR stay attached to their lines.

Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
@Denis-hamon

Copy link
Copy Markdown
Contributor Author

Human review — Does not work

C3 · Does the elapsed time come from the CLI or from the API?
Dimension: Product fit

Expected: The CLI times itself and ATTRIBUTES the API figure to the API rather than asserting it. Does that distinction read clearly to you?

🛑 error reinstalling server: mandatory field "operatingSystem" is missing in the parameters

Usage:
ovhcloud baremetal reinstall <service_name> [flags]

— Denis (Product Manager), through CLI sandbox review page

A --dry-run already carries the parameters in its message, and a
log.Println sitting just above the branch repeated the same JSON behind
a Go timestamp no other command in this CLI emits:

    🔍 Dry run: nothing was sent. This would have been posted to …
    { "operatingSystem": "debian12_64" }
    2026/08/23 23:47:22 Final parameters:
    { "operatingSystem": "debian12_64" }

The log line moves below the branch. A real run still logs what it is
about to send, which is what it was for; a dry run logs nothing, because
it sends nothing.

The line goes to stderr, so no assertion on stdout could ever have seen
it — which is why it survived every green run. Two tests now redirect the
logger: one that a dry run does not log, and its positive control that a
real run still does, so deleting the line outright would not pass.

Signed-off-by: Denis <denis.hamon@ovhcloud.com>
Mechanical, not a change of behaviour. Placing them just after
TestBaremetalReinstallDryRun put them in the one region of
baremetal_test.go that 23 downstream branches also append to, and in the
import block they all touch as well. Merging the parent into those
branches failed 23 times out of 23, every one of them on adjacency
rather than on a disagreement.

baremetal_test.go goes back to what it was before the previous commit,
so this branch now leaves that file untouched. A new file can only clash
with a file of the same name, and nothing else carries this one.

Signed-off-by: Denis <denis.hamon@ovhcloud.com>
Signed-off-by: Denis <denis.hamon@ovhcloud.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant