Skip to content

feat: replace fog-cloudstack with cloudstack_client - #62

Draft
tas50 wants to merge 1 commit into
mainfrom
convert-to-cloudstack-client
Draft

feat: replace fog-cloudstack with cloudstack_client#62
tas50 wants to merge 1 commit into
mainfrom
convert-to-cloudstack-client

Conversation

@tas50

@tas50 tas50 commented Aug 24, 2026

Copy link
Copy Markdown
Member

Moves the driver off fog-cloudstack and onto cloudstack_client.

Important

Draft: blocked on niwo/cloudstack_client#20. cloudstack_client 1.6.0 cannot be used as a library — see below. The Gemfile currently points at a branch of my fork so this is testable; that override comes out and the gemspec gets pinned to a real release before this merges.

Why

fog-cloudstack has had exactly one release, 0.1.0, on 2019-06-17. cloudstack_client is maintained, and it has no runtime dependencies at all, so this removes fog-core, fog-json, fog-xml, excon and nokogiri from the dependency tree with nothing taking their place:

$ grep -E "fog|excon|nokogiri|cloudstack_client" Gemfile.lock
    cloudstack_client (1.6.0)

Driver configuration is unchanged — every cloudstack_* setting behaves as before.

The upstream blocker

cloudstack_client calls ActiveSupport's present? on every request without depending on ActiveSupport, so the first API call raises NoMethodError — reported, misleadingly, as an unreachable endpoint. require "base64" is also a LoadError on Ruby 3.4+. Both are fixed in niwo/cloudstack_client#20, along with making TLS verification configurable.

Things that needed care

cloudstack_client validates arguments against a bundled CloudStack 4.5 API definition and silently drops anything it doesn't recognise. Three live behaviours were affected:

Custom service offering sizing. Sent flat, these are dropped, and instances silently come up with the offering's default CPU and memory:

"details[0].cpuNumber"  -> sent as details[0].cpuNumber  *** DROPPED ***
"details[0].cpuSpeed"   -> sent as details[0].cpuSpeed   *** DROPPED ***
"details[0].memory"     -> sent as details[0].memory     *** DROPPED ***

They now go through CloudStack's details map, which is the documented shape and serialises to the identical wire format:

details[0].cpuNumber=2
details[0].cpuSpeed=2000
details[0].memory=4096

There were no tests covering this, so this PR adds some.

openfirewall and expunge are now sent as strings. cloudstack_client drops falsey values outright:

boolean false    -> *** NOT SENT ***
string "false"   -> openfirewall=false

That matters because openfirewall defaults to true on a non-VPC network. A dropped false would let CloudStack open the firewall itself, creating a rule that teardown doesn't know about and so never removes.

cloudstack_project_id. fog applied this to every request from the connection object. cloudstack_client has no equivalent, so it's now passed explicitly to the four commands that accept it — deployVirtualMachine, listVirtualMachines, associateIpAddress, listNetworks. It was also being sent to createFirewallRule, which has no such parameter, so that's dropped.

Async handling

cloudstack_client can wait for jobs itself, but returns only the finished job result — the caller never sees the id of the resource being created. The driver records that id in state before waiting, so an interrupted kitchen create still leaves enough state to destroy what it started.

So every async call passes sync: true and Client#run_job keeps doing the waiting. This also keeps cloudstack_job_poll_interval and cloudstack_job_timeout meaningful, which the gem's own minimums (60s timeout, 1.0s interval) would otherwise constrain.

One improvement

disable_ssl_validation no longer sets Excon.defaults[:ssl_verify_peer] = false, which disabled certificate verification process-wide for anything else sharing the process. It's now scoped to this driver's connection, and it applies to kitchen doctor too — which previously couldn't check an endpoint using a self-signed certificate.

Testing

85 examples, 0 failures
9 files inspected, no offenses detected

spec/integration/lifecycle_spec.rb still runs a full create/status/destroy cycle with only the HTTP layer stubbed (retargeted from Excon to Net::HTTP), so real request signing and response parsing are exercised. It caught a genuine bug while I was writing it: my stub omitted the count field, which is what cloudstack_client's unwrapping keys off when deciding whether to return a bare array.

Before merging

  • Fix using the gem outside of cloudstack-cli niwo/cloudstack_client#20 merged and released
  • Remove the Gemfile git override
  • Pin the gemspec to the released version
  • Consider whether the bundled 4.5 API definition is a long-term concern — all 12 commands this driver uses are covered, but a parameter added after CloudStack 4.5 will be dropped silently

fog-cloudstack has had exactly one release, 0.1.0, in June 2019. This
moves the driver onto cloudstack_client, which is maintained and which
has no runtime dependencies at all, so fog-core, fog-json, fog-xml,
excon and nokogiri all leave the dependency tree with nothing taking
their place.

The driver's configuration is unchanged. Internally:

- Client#compute becomes Client#api and returns a CloudstackClient::Client.
- cloudstack_client strips the response envelope, so responses are read
  directly rather than through fetch("listzonesresponse") and friends.
- Every asynchronous call passes sync: true. cloudstack_client can wait
  for jobs itself, but hands back only the finished job result, and the
  driver needs the id of the resource being built so it can record it in
  state before waiting -- otherwise an interrupted create leaves a VM
  behind with nothing in state to destroy it by. Client#run_job keeps
  doing the waiting, so cloudstack_job_poll_interval and
  cloudstack_job_timeout keep working.
- Teardown rescues CloudstackClient::ApiError in place of
  Fog::Cloudstack::Compute::BadRequest.

Three things needed care because cloudstack_client validates arguments
against a bundled CloudStack 4.5 API definition and silently drops
anything it does not recognise:

- Custom service offering sizing moves from flat details[0].cpuNumber
  parameters to CloudStack's details map, which is the shape the API
  documents. Sent flat they were dropped, and instances came up with the
  offering's default CPU and memory.
- openfirewall and expunge are sent as strings. cloudstack_client drops
  falsey values, and openfirewall defaults to true on a non-VPC network,
  so a dropped "false" would let CloudStack open the firewall itself and
  create a rule teardown does not know about.
- cloudstack_project_id was applied by fog to every request from the
  connection. cloudstack_client has no equivalent, so it is now passed
  to the four commands that accept it: deployVirtualMachine,
  listVirtualMachines, associateIpAddress and listNetworks. It was also
  being sent to createFirewallRule, which has no such parameter, so that
  is dropped.

disable_ssl_validation no longer sets Excon.defaults[:ssl_verify_peer],
which turned verification off process-wide for anything else sharing the
process. It is now scoped to this driver's connection, and it applies to
kitchen doctor as well, which previously could not check an endpoint
using a self-signed certificate.

The integration spec still runs a full create/status/destroy cycle with
only the HTTP layer stubbed, so request signing and response parsing are
exercised for real.

Signed-off-by: Tim Smith <tim@mondoo.com>
@tas50
tas50 force-pushed the convert-to-cloudstack-client branch from 6da516a to 3410e6e Compare August 24, 2026 21:26
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