Skip to content

Add several features missing from OpenStack::MetaAPI - #28

Merged
atoomic merged 6 commits into
cpan-authors:masterfrom
troglodyne:honour-passed-auth
Sep 11, 2026
Merged

atoomic merged 6 commits into
cpan-authors:masterfrom
troglodyne:honour-passed-auth

Conversation

@troglodyne

Copy link
Copy Markdown
Contributor
  • Adds support for clouds.yaml usage for auth, very useful in CI context or for use with Trog::Provisioner (soon).
  • Make floating IPs optional, because they are (and certainly at cPanel our setup now makes it optional).
  • Fixes bug in uploads of cloud-config (it needed the content to get B64'd)
  • Fleshes out the API surface more, as it was only partially implemented here.

Related PR (what I wrote this to serve):
Troglodyne-Internet-Widgets/trog-provisioner#121

That said I probably can use this within cPanel's QA testsuite or other CI workflows we have for those cases where terraform's openstack driver just doesn't have the functionality to get what you want done (as it generally only handles vm spawn/lifecyle, not things like image management, etc.)

troglodyne and others added 5 commits September 10, 2026 16:32
BUILDARGS built an OpenStack::Client::Auth from its arguments unconditionally
and discarded everything else, so the 'auth' attribute -- declared required,
and therefore apparently settable -- could not in fact be set.

That shut out every authentication method OpenStack::Client::Auth does not
implement.  Keystone v3 application credentials are the case in hand: there is
no username or password to hand over, so the only way to authenticate is to
construct the auth object yourself, and that was the one thing there was no way
to pass in.

A lone hashref carrying a reference in 'auth' now goes straight through.
Ordinary construction is new($endpoint, %args), whose first argument is a URL
rather than a reference, so no existing caller matches this and none of them
change behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five things the API could not do, each needed to manage a server's whole life
rather than only create and delete it.

limits is the interesting one.  Deciding whether another instance fits needs
both the project's allowance and what it is already using, and /limits returns
them together -- where /os-quota-sets gives only the allowance and leaves the
caller to total up a server list to find the rest.

server_action covers everything Nova models as an action on an existing server:
snapshots, power state, rebuild.  They are one POST to one path with a
different single-key body, so they are one method.  create_image is that, named,
because snapshotting is the case people come looking for; it deliberately does
not report the new image's id, since Nova returns it in a Location header and
only puts it in the body from microversion 2.45, so neither is worth relying
on.  Ask Glance by name instead.

flavors_detail exists because the flavor list is ids and names.  Anything that
has to choose a flavor by how much machine it is needs /flavors/detail.

console_output is what a guest that never came up said before it gave up.

There was no block storage service at all, so volumes could be neither made,
found nor deleted.  Its catalogue type is whatever the cloud still offers,
newest first, rather than a name fixed here: 'volume' unadorned is not always
present, and a current deployment may list only volumev3 and volumev2.  Its two
list routes are written out rather than declared in a Specs data block, because
the spec package a service loads is named after its catalogue type -- so the
same two routes would need one spec file per spelling of Cinder.

Attaching a volume is Nova's endpoint rather than Cinder's, so attach_volume,
detach_volume and server_volumes are on compute where they belong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
create_vm could build a server but not tell it anything, because user_data was
not among the fields it passed on.  That is most of the point of creating one:
without it a new guest boots with no cloud-config, no users, no keys and no way
to be told what it is for, and the caller's only recourse was to bypass create_vm
and assemble the request itself -- losing the flavor and network lookups, the
wait for ACTIVE and the floating IP along with it.

Nova only accepts user_data base64 encoded, and callers have cloud-config
rather than base64, so it is encoded here.  Making each caller remember is the
kind of detail that is discovered by a guest silently booting unconfigured.

availability_zone, metadata and block_device_mapping_v2 come along for the same
reason: each is an ordinary server-create field with no way to set it.  They are
only sent when asked for, so a request that did not mention them is unchanged.

The security group was being sent as {name => $group->{id}}, which puts an id in
a field that means a name.  Some clouds resolve either and some do not, so it
now sends the name and falls back to the id.

The test asserts on the request body rather than the response, which is what
required a way to see it: for a field the API encodes on the caller's behalf,
the encoding is the behaviour, and the canned response would exercise none of
it.  Hence last_http_content in the test helper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
images() died pointing at image_from_uid and image_from_name, and the warning
behind that is real: listing a project's images unfiltered can take fifty
requests.  The cloud this was written against holds 632 of them.

But some questions have no single-image answer, and "which snapshots does this
guest have" is one -- Glance keeps them as ordinary images, so finding them
means asking for the ones that match.  With no filtered listing available, the
only way to answer it was to not use this module.

list_images passes %query to Glance, so the filtering happens there rather than
in the caller: name, owner, visibility, status, or any image property.  Nova
stamps its snapshots with image_type and instance_uuid, which is what makes the
question answerable in one request instead of 632 images' worth of pages.

Pagination is not new work either; OpenStack::Client::all already follows the
'next' link, and had no way of being pointed at images.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
create_vm demanded network_for_floating_ip and always attached a floating IP,
which made a whole shape of cloud impossible to build on.

Not every deployment has a tenant network to escape from.  Where the only
network is external and shared -- which is how the cloud this was found on is
arranged -- a server attached to it is given a routable address directly, and
Nova reports that address as 'fixed'.  There is no floating IP to allocate, no
pool to allocate it from, and asking for one is an error rather than a
formality.

So the argument is optional now, and the floating IP is attached only when a
network was named to take it from.  A caller that named one is unaffected, and
the existing failure for a network that does not exist still fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Koan-Bot

Koan-Bot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@Koan-Bot Koan-Bot 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.

Warning

Important issues found.

  • BUILDARGS auth-passthrough uses ref, not blessed — a clouds.yaml hash silently becomes the auth object

@atoomic

atoomic commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

note: #29 will fix CI
let's focus on the #1 issue there

The auth-passthrough guard checked ref $args[0]->{'auth'}, which is true
for any reference including a plain HASH. A clouds.yaml cloud entry is a
hashref whose own 'auth' key is an unblessed hash (auth_url, username,
password, ...), so it was accepted verbatim as the auth object: Moo stored
the unblessed hash and construction succeeded, deferring the failure to the
first delegated call, which died on an unblessed reference far from here.

Check blessed() instead so a config hash falls through and builds a real
OpenStack::Client::Auth, which dies legibly at construction. Add a test
covering the clouds.yaml-shaped hash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@troglodyne

Copy link
Copy Markdown
Contributor Author

Addressed, thanks

@Koan-Bot

Copy link
Copy Markdown
Contributor

@atoomic: note: #29 will fix CI

Issue #1 resolved. BUILDARGS now guards on blessed($args[0]->{'auth'}), not ref. A clouds.yaml cloud entry falls through to OpenStack::Client::Auth->new as before. New negative test at t/01_base.t covers {auth => {username => 'x'}}. Nothing blocking left — remaining findings are all suggestions.

@Koan-Bot

Copy link
Copy Markdown
Contributor

@troglodyne: Addressed, thanks

Verified. blessed() guard correct, test covers the unblessed case. Three earlier suggestions still open: list_images has no test and returns a bare list (count in scalar context), the three volume-attachment methods skip the $uid guard their siblings have, and POD/Changes/dist.ini prereqs cover none of the new surface.

@Koan-Bot

Copy link
Copy Markdown
Contributor

PR Review — Add several features missing from OpenStack::MetaAPI

A substantial, well-tested expansion of the API surface — fourteen new routes, a Cinder service class, base64 user_data, and an optional floating IP — with the previously blocking BUILDARGS guard now correctly fixed. Everything remaining is non-blocking.

  • BUILDARGS now uses blessed(), and the new negative test pins that a clouds.yaml-shaped auth hash still falls through to OpenStack::Client::Auth
  • Volume.pm resolving volumev3/v2/volume from the live catalogue instead of a hardcoded type is the right call, and the fixture covers all three
  • Asserting on the captured POST body rather than the response is the correct level for a field the API encodes on the caller's behalf
  • list_images remains the only new route with no test, and returns a bare list where its siblings collapse to a scalar
  • POD, README, Changes, and the dist.ini prereq list still cover none of the new surface

✅ Resolved since last review (2)

Previously-flagged issues verified fixed
  • lib/OpenStack/MetaAPI.pm:57 BUILDARGS auth-passthrough uses ref, not blessed — a clouds.yaml hash silently becomes the auth object
  • lib/OpenStack/MetaAPI.pm:233 POD and Changes not updated for the new surface

🟢 Suggestions

1. list_images is still the only new route with no test, and returns a bare list
lib/OpenStack/MetaAPI/API/Images.pm:48-51

This was raised on the previous revision and is unchanged. t/service-extras.t covers the other twelve new routes; list_images has none — and it is the method the amended images() die message now points users at.

Two things a test would pin down:

  • Return shape breaks the house convention. OpenStack::Client::all ends in return @items, so list_images returns a raw list. Every other lister here goes through Roles::Listable::_list, which collapses to a scalar for <=1 result (lib/OpenStack/MetaAPI/Roles/Listable.pm:71). So my $snaps = $api->list_images(instance_uuid => $uid) — the query the comment directly above advertises — yields the count, not a hashref or arrayref. Routing through _list(['/images', 'images'], \%query) would give the same pagination plus the collapse, the regex filters, and the spec-driven query-param handling for free. Note _list's $uri !~ m{^/v} guard means /images would still reach root_uri and resolve to v2/images, same as today.
  • The URI is worth pinning. root_uri('/images') yields v2/images only because version_prefix is 'v2' and the Glance endpoint carries no version (lib/OpenStack/MetaAPI/API/Service.pm:79-98). A last_http_request() assertion, as service-extras.t uses elsewhere, would lock that in.
sub list_images {
    my ($self, %query) = @_;

    return $self->client->all($self->root_uri('/images'), 'images', \%query);
}
2. server_volumes / attach_volume / detach_volume still don't validate $uid
lib/OpenStack/MetaAPI/API/Compute.pm:123-134

Also unchanged from the previous revision. server_action (line 78), create_image (line 93), and every Cinder method in the new Volume.pm guard their required ids with unless defined ... && length .... These three guard $volume_id but never $uid.

With $uid undef, root_uri("/servers/$uid/os-volume_attachments") interpolates an empty string — a Use of uninitialized value warning under use warnings — and the request lands on .../v2.1/servers//os-volume_attachments. The caller gets a Nova 404 instead of the clear die its siblings produce.

One line each keeps the new surface internally consistent:

die "server id is required by attach_volume" unless defined $uid && length $uid;
sub server_volumes {
    my ($self, $uid) = @_;

    my $out = $self->get($self->root_uri("/servers/$uid/os-volume_attachments"));
3. block_device_mapping_v2 can be passed but cannot actually boot from a volume
lib/OpenStack/MetaAPI.pm:124-129

The comment above this loop says the field carries "the volumes it boots or carries" — but the boot case is unreachable through create_vm.

  • image is still a hard requirement (die "'image' name or id is required by create_vm" at line 57), and imageRef => $image->{id} is unconditionally included in the create_server payload at line 92.
  • Nova rejects a request that carries both a non-empty imageRef and a block_device_mapping_v2 entry with boot_index: 0 sourced from a volume or snapshot, with 400 Block Device Mapping is Invalid: Boot sequence for the instance and image/block device mapping combination is not valid.

So the pass-through works for attaching extra volumes at create time, which is genuinely useful, but a caller following the comment into a boot-from-volume call gets a 400 with no hint from this module about why.

Either narrow the comment to what the field can actually do here, or let image be omitted when a boot_index: 0 mapping is present and send imageRef => '' in that case.

    foreach my $field (qw{availability_zone metadata block_device_mapping_v2}) {
        push @extra, ($field => $opts{$field}) if defined $opts{$field};
    }
4. POD, README, Changes, and dist.ini prereqs not updated for the new surface
lib/OpenStack/MetaAPI.pm:275-288

Still open from the previous revision. README.md is generated from this POD ([ReadmeAnyFromPod] in dist.ini), so the POD is this distribution's only user-facing documentation. The PR adds fourteen routes, a new API::Volume service, and four create_vm options, none of them documented:

  • The create_vm synopsis still lists network_for_floating_ip as a plain argument with no note that it is now optional — a headline change of this PR.
  • user_data, availability_zone, metadata, and block_device_mapping_v2 are undiscoverable. user_data in particular is base64-encoded for the caller; double-encoding is the natural mistake and produces a VM that boots with garbage cloud-init.
  • "Available functions / methods" lists every existing route; the new ones are absent, including the whole Cinder surface.
  • Changes still carries only the {{NEXT}} stanza.
  • MIME::Base64 is now a runtime dependency and isn't under [Prereqs / RuntimeRequires] in dist.ini. It is core so nothing breaks, but that block is hand-maintained (AutoPrereqs is off), so declaring it keeps the list honest.
=head2 $api->create_vm( %args )

Create one server from one image with one floating IP, wait for the server to be ready.

Checklist

  • Input validation at boundaries — suggestion #2
  • New branches covered by tests — suggestion #1
  • Public API docs kept in sync with code — suggestion #4
  • Return conventions consistent across the object — suggestion #1
  • Auth object passthrough rejects unblessed config hashes
  • Backward compatible for existing create_vm callers
  • New routes resolve to real service methods without name collisions
  • No hardcoded secrets or credentials

Automated review by Skuggi HEAD=d10099a 7 min 30s

@Koan-Bot Koan-Bot 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.

Tip

No blocking issues found — ready to merge.

@atoomic
atoomic merged commit 0740875 into cpan-authors:master Sep 11, 2026
19 checks passed
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.

3 participants