feat(baremetal): make the storage block of a reinstall discoverable - #250
feat(baremetal): make the storage block of a reinstall discoverable#250Denis-hamon wants to merge 20 commits into
Conversation
…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>
Partitioning was reported as unreachable from this CLI. It is not: `--from-file` carries the whole `storage` block — hardware RAID, layout, LVM and ZFS extras — untouched into the request body, verified with `--dry-run` against the live API, and the sample this CLI ships already shows a full layout. So this adds no partitioning flags. `storage` is an array of objects each holding an array of partitions with five fields; expressing that as command-line flags is how a CLI becomes unusable, and the file already works. What was missing is every way of finding out what to put in it. ovhcloud baremetal list-partition-schemes <server> --os <template> ovhcloud baremetal raid-profile <server> The first is asked per template, because that is what the answer depends on. The second answers a question worth asking before writing a `hardwareRaid` block: every one of the fourteen servers checked on a real account has no controller, and the API says so with a 403. That is an answer, not a failure — it means software RAID in the partitioning layout — so it is reported as one, while any other error stays an error. There is no command for sizing a RAID configuration. `install/hardwareRaidSize` is in the embedded schema, badged "Stable production version", and does not exist: it answers 404 "Got an invalid (or empty) URL" — a routing error, not a business one — on four different servers, in v1 and v2, with and without its parameters, while `install/hardwareRaidProfile` resolves on those same servers. A command for it would have failed every time it was run. Worth reporting to whoever owns that schema. And the discovery path itself was broken. `--init-file` always opened a picker, which needs a terminal, so the one way to obtain the example that documents the `storage` shape failed with "could not open a new TTY" in exactly the scripted context it serves, and wrote nothing. Outside a terminal it now writes one example and says which: the "default" one this CLI ships, since for a reinstall that is the only one carrying a storage block, and otherwise the first by name — sorted, because the examples are a map and ranging over it would produce a different file on each run. Two things about that fix are worth knowing beyond this change. `--init-file` runs in a PreRun that ends with `os.Exit(0)`, and that exit is load-bearing: without it the command would go on to run, so `--init-file` on a reinstall would reinstall the server. And `display.OutputWarning` terminates the process — it sets Warning, which reaches ExitFunc(0) — so the first version of this printed its message and never wrote the file. It uses `log` now. Seven tests. Four sabotages, four failures; two more did not compile and were discarded rather than counted. Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A hostname, an address, an order id and a colleague's name read off a live account had been used as fixtures and as examples in comments. This repository is public: what goes in stays in. The values are replaced with synthetic ones of the same shape — RFC 5737 documentation addresses, hostnames built on them, identifiers of the same length — so the tests keep exercising the same parsing. This is the fourth time in this series that live account data reached a commit, after a live IP migration token, a test server's address, and a real invoice id. The first three were fixed one at a time as they were noticed; this is the sweep that should have followed the first one. 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>
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>
`list-partition-schemes` and `raid-profile` both register --filter through withFilterFlag, and both then called display.RenderTable directly. RenderTable does not filter: withFilterFlag only binds the flag to flags.GenericFilters, and ManageListRequest is what normally passes it to filters.FilterLines. These two build their rows themselves, so nothing ever read the flag. It was accepted by cobra, written into doc/ by docgen, and inert — the operator reads a list they believe was narrowed. An audit of every withFilterFlag call site in the tree found the same shape on twenty-one commands this series added, so the three lines are extracted rather than copied again: common.RenderFilteredTable filters, then renders. The `internal/services/ip` package already carried a private copy of exactly these lines; it is left alone, since it does the right thing, but the shared one is where new commands should reach. The tests assert the row the filter EXCLUDES is absent, not merely that the kept row is present — the second passes just as well with the filtering removed. Both go red when the helper is made to ignore the flag. 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>
Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
Same behaviour, one function more. It exists for the callers that do not render a table: a few wrap their list in an object and hand it to a template, and there the filter has to run before the wrapping — once the list is inside an object there are no rows left to select from. None of those callers are on this branch. The split is here so that this helper is byte-identical to the one the same defect required on `main`, where twenty commands of cloud, vps and webhosting had the same inert flag and two of them are exactly that template-wrapping shape. The two patches touch the same lines of the same file; identical bodies make the merge a no-op instead of a conflict someone has to arbitrate. Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
capacity and speed are complexType.UnitAndValue in the schema, not scalars, and they were declared `any`. So they reached the table as the decoded map and rendered as "map[unit:GB value:1000]", truncated to the cell width — on every server that actually has a hardware RAID controller, which is none of the fourteen measured. That is why a wrong column shipped unseen. Both shapes are accepted, and the reason is that the object shape cannot be confirmed from here: hardwareRaidProfile answers 403 on all 35 servers of the account, so nobody has seen a populated response. The schema says object, the fixture this test file already carried assumed a bare string. Decoding into a struct alone would have turned an ugly cell into a broken command if the API sends the scalar; a small UnmarshalJSON takes either and cannot be wrong. An absent quantity renders empty rather than as a zero the API never sent. speed gains a column while it is here: it was read, discarded, and would have rendered the same way. Two tests. The first sabotage was inert and said so — replacing .String() with the value changes nothing, because the type implements Stringer, which is the proof that the type is the fix. Reverting the fields to `any` turns both red. 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>
Signed-off-by: Denis Hamon <denis.hamon@ovhcloud.com>
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>
Human review — DoubtC5 · Finding a partition scheme without reading the docs
┌─────────┬─────────────┐ — Denis (Product Manager), through CLI sandbox review page |
|
Answering the doubt on C5. The single row is genuine, not a truncated list: that server accepts exactly one partition scheme for One thing did change underneath, in #241: an empty or near-empty table used to print its headers with no padding at all, because only data cells carried it. That is fixed, so a short table now reads the same as a long one. |
Human review — DoubtC5 · Finding a partition scheme without reading the docs
┌─────────┬─────────────┐ — 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>
…' into feat/baremetal-install-discovery
Signed-off-by: Denis <denis.hamon@ovhcloud.com>
Stacked on #249 — only the last commit (
201de18) belongs to this PR.Partitioning was not blocked
The audit this work follows lists partitioning as the costliest case of "the CLI exposes only the fields mapped to flags". Measured against the live API, that premise is wrong:
--from-filecarries the wholestorageblock — hardware RAID, layout, LVM and ZFS extras — untouched into the request body.--dry-runshows it going out verbatim. And the sample this CLI ships already contains a full partitioning layout.So this adds no partitioning flags.
storageis an array of objects each holding an array of partitions with five fields; expressing that as flags is how a CLI becomes unusable. The file is the right instrument and it already works.What was missing is every way of finding out what to put in it. On a reinstall, finding out at install time means the disks are already wiped.
Two commands, and one deliberately absent
list-partition-schemesis asked per template, because that is what the answer depends on — hence--osis required rather than silently defaulted.raid-profileanswers the question you must answer before writing ahardwareRaidblock. All fourteen servers checked on a real account have no controller, and the API says so with a 403:That is an answer, not a failure, so it is reported as one — while any other error stays an error, and there is a test for each.
There is no command for sizing a RAID configuration, on purpose.
install/hardwareRaidSizeis in the embedded schema and badged "Stable production version". It does not exist:hardwareRaidSizewith both required paramsGot an invalid (or empty) URLhardwareRaidProfile, same servers (control)A routing 404 next to a sibling that resolves means the route is not served. A command for it would have failed every single time it ran. Worth reporting to whoever owns that schema.
The discovery path itself was broken
--init-fileis how somebody writing a script obtains the example that documents the request body. It always opened a picker, which needs a terminal — so in exactly the scripted context it serves it failed withcould not open a new TTYand wrote nothing.Outside a terminal it now writes one example and says which: the
"default"one this CLI ships, since for a reinstall that is the only one carrying a storage block; otherwise the first by name, sorted, because the examples are a map and ranging over it would write a different file on each run.The whole loop now runs unattended:
Two findings worth carrying beyond this PR
--init-fileruns in aPreRunending withos.Exit(0), and that exit is load-bearing: without it the command would continue intoRun, so--init-fileon a reinstall would reinstall the server. It is not a testability wart to remove.display.OutputWarningterminates the process — it setsWarning, which reachesExitFunc(0). The first version of this fix printed its message and never wrote the file. Anything after anOutputWarningis dead code, the same way it is afterOutputError.Seven tests. Four sabotages, four failures; two more did not compile and were discarded rather than counted.
doc/ovhcloud_logout.mdremains untracked and unseen by the doc gate —git diff --exit-codedoes not look at untracked files. Not this change's business, but the hole is still there.🤖 Generated with Claude Code