Skip to content

Conversation

@fank
Copy link

@fank fank commented Nov 8, 2025

Summary

Fixes JSON serialization issues with the Vultr cloud provider's startup script handling.

Problem

The startup_script module was failing with:

Object of type 'bytes' is not JSON serializable by the 'tagless' profile.

This occurred due to complex interactions between:

  • Nested template lookups in cloud-init/base.yml
  • The "tagless" JSON serialization profile in Ansible 2.19+
  • The vultr.cloud.startup_script module's base64 encoding logic

Solutions

1. Two-Step Fact Assignment

Implemented the Linode provider pattern: evaluate the template into a fact first, then pass that fact to the module. This ensures the template is fully evaluated before JSON serialization.

Change:

- name: Set cloud-init script as fact
  set_fact:
    algo_cloud_init_script: "{{ lookup('template', 'files/cloud-init/base.yml') }}"

- name: Upload the startup script
  vultr.cloud.startup_script:
    name: algo-startup
    script: "{{ algo_cloud_init_script }}"

2. Cloud-Init Template String Filters

Added | string filters to nested lookups in files/cloud-init/base.yml to ensure consistent string handling:

# SSH config template
{{ lookup('template', 'files/cloud-init/sshd_config') | string | indent(width=6, first=True) }}

# SSH public key
- "{{ lookup('file', SSH_keys.public) | string }}"

Known Issue - Dependency Bug

IMPORTANT: There is a bug in vultr.cloud collection v1.13.0 that prevents this from working completely.

The module's configure() method calls:

self.module.params["script"] = base64.b64encode(self.module.params["script"].encode())

The base64.b64encode() returns bytes, which cannot be JSON serialized. It should be:

self.module.params["script"] = base64.b64encode(self.module.params["script"].encode()).decode('ascii')

Upstream bug report: Will be filed at https://github.com/vultr/ansible-collection-vultr/issues/

Until this is fixed upstream, users will need to manually patch the module or wait for a new release.

Testing

  • ✅ Cloud-init template evaluates without undefined markers
  • ✅ All nested template lookups return strings
  • ⚠️ Startup script upload requires patched vultr.cloud module

Impact

  • Improves robustness of cloud-init template for all providers
  • Follows established patterns from other cloud providers (Linode)
  • Backward compatible - all changes are defensive improvements
  • Blocked by upstream bug in vultr.cloud collection

Files Changed

  • roles/cloud-vultr/tasks/main.yml - Two-step fact assignment for startup script
  • files/cloud-init/base.yml - String filters on nested lookups (shared by all providers)

Technical Details

The vultr.cloud.startup_script module (v1.13.0) calls base64.b64encode(script.encode()) internally. When combined with Ansible 2.19's "tagless" JSON serialization profile, certain template evaluation patterns can result in bytes objects that fail serialization. The two-step fact assignment ensures the template is fully evaluated to a string before entering the module's processing pipeline.

However, the module itself has a bug that creates bytes that cannot be serialized. This PR contains the Algo-side fixes, but full functionality depends on the upstream fix.

fank added 3 commits November 8, 2025 10:25
The startup_script module was failing with "Object of type 'bytes' is not
JSON serializable" because the lookup('template', ...) was returning bytes
instead of a string.

Added | string filter to explicitly convert the template result to a
string, matching the pattern used by the DigitalOcean cloud provider.

Also simplified from multiline block format to inline format for
consistency with other cloud providers.

Fixes the error: "Object of type 'bytes' is not JSON serializable by the
'tagless' profile."
Added | string filters to lookup() calls in the cloud-init base template
to ensure consistent string handling across all cloud providers.

The Vultr startup_script module requires all values to be JSON-serializable
strings, and lookup() can return bytes in some contexts. This change ensures
that both the SSH config template lookup and the SSH public key file lookup
explicitly return strings.

This is a defensive fix that improves compatibility with strict JSON
serialization requirements in some Ansible modules, while remaining
backward compatible with existing cloud providers.

Related to: vultr.cloud.startup_script JSON serialization requirements
Changed to set the cloud-init script as a fact first, then reference
that fact in the startup_script module. This follows the pattern used
by the Linode provider and avoids JSON serialization issues with nested
template lookups.

This approach ensures the template is fully evaluated and stored as a
string before being passed to the vultr.cloud.startup_script module,
which then base64-encodes it for the API.

Related to: JSON serialization with "tagless" profile in Ansible 2.19+
@fank fank requested a review from jackivanov as a code owner November 8, 2025 09: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