Skip to content

🐞 Bug: vm_disk, vm_nic, vm_replication: PATCH payloads include read-only parent-reference fields rejected by HyperCore v2 API #368

Description

@ddemlow

Summary

Three modules send fields on PATCH that HyperCore's v2 API (/rest/v2) rejects with HTTP 400. The v1 API silently accepted these fields; v2 uses additionalProperties: false on all PATCH schemas, making them hard rejections. All
three fields are parent-reference fields set at creation time that cannot be changed via PATCH — they are being sent unnecessarily because the collection reuses the same serialization method for both POST (create) and PATCH
(update).

This was discovered during a systematic compatibility audit of the collection against HyperCore v2/9.7.1 using authenticated OpenAPI spec comparison and Ansible collection source analysis.


Affected modules and fields

┌────────────────┬────────────────────────────────────┬─────────────────────┬─────────────────────────┬────────────────────────────┐
│ Module │ Endpoint │ Field sent on PATCH │ Should be sent on POST? │ Can be sent on PATCH (v2)? │
├────────────────┼────────────────────────────────────┼─────────────────────┼─────────────────────────┼────────────────────────────┤
│ vm_disk │ PATCH /VirDomainBlockDevice/{uuid} │ virDomainUUID │ ✅ yes │ ❌ 400 │
├────────────────┼────────────────────────────────────┼─────────────────────┼─────────────────────────┼────────────────────────────┤
│ vm_disk │ PATCH /VirDomainBlockDevice/{uuid} │ slot │ ✅ yes │ ❌ 400 │
├────────────────┼────────────────────────────────────┼─────────────────────┼─────────────────────────┼────────────────────────────┤
│ vm_nic │ PATCH /VirDomainNetDevice/{uuid} │ virDomainUUID │ ✅ yes │ ❌ 400 │
├────────────────┼────────────────────────────────────┼─────────────────────┼─────────────────────────┼────────────────────────────┤
│ vm_replication │ PATCH /VirDomainReplication/{uuid} │ sourceDomainUUID │ ✅ yes │ ❌ 400 │
└────────────────┴────────────────────────────────────┴─────────────────────┴─────────────────────────┴────────────────────────────┘

virDomainUUID (on both disk and NIC) and sourceDomainUUID (on replication) are the UUID of the parent VM — a relationship established at creation that cannot be reassigned via PATCH. slot specifies disk position at creation.


Root cause — exact locations

  1. plugins/module_utils/disk.py — post_and_patch_payload()

to_hypercore() unconditionally returns virDomainUUID and slot. post_and_patch_payload() has partial slot-stripping logic for nvram/vtpm types but retains both fields for all standard disk types (virtio_disk, ide_disk, scsi_disk,
ide_cdrom, ide_floppy). This payload is used directly as the PATCH body.

  1. plugins/module_utils/nic.py — to_hypercore()

def to_hypercore(self):
return {
"virDomainUUID": self.vm_uuid, # ← sent on every PATCH
"vlan": self.vlan,
...
}

Used for both POST (create NIC) and PATCH (update NIC) without filtering.

  1. plugins/module_utils/replication.py — to_hypercore()

def to_hypercore(self):
return {
"sourceDomainUUID": self.vm_uuid, # ← sent on every PATCH
"connectionUUID": ...,
"enable": ...,
}

Used directly as the PATCH body.


Integration test targets that fail against v2/9.7.1

Of 69 integration test targets analysed, 14 fail due to these three issues:

┌──────────────────────┬───────────────────────────────────────────────────────────────────────┐
│ Target │ Failing operation │
├──────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ vm_disk │ Disk update PATCH → virDomainUUID + slot │
├──────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ vm_disk__iso_eject │ ISO detach via iso_image_management() → disk PATCH with virDomainUUID │
├──────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ vm_disk__nvram_vtpm │ slot is stripped for these types but virDomainUUID remains │
├──────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ vm_nic │ NIC update PATCH → virDomainUUID │
├──────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ vm_replication │ Replication update PATCH → sourceDomainUUID │
├──────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ vm_clone__replicated │ Post-clone replication setup → sourceDomainUUID PATCH │
├──────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ vm │ Exercises both disk and NIC update paths │
├──────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ vm__iso_eject │ ISO management hits disk PATCH with virDomainUUID │
├──────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ virtual_disk_attach │ Attach template body includes virDomainUUID + slot │
├──────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ git_issue_3 │ Disk enlarge → disk PATCH with banned fields │
├──────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ git_issue_15 │ Tiering priority update → disk PATCH with banned fields │
├──────────────────────┼───────────────────────────────────────────────────────────────────────┤
│ role_template2vm │ Role calls vm_disk resize + vm_nic set — both broken │
└──────────────────────┴───────────────────────────────────────────────────────────────────────┘

55 of 69 targets are unaffected. Notable safe targets: vm__remove_disk (DELETE only), vm_clone (POST only), vm__rename/vm__machine_type/vm_boot_devices/vm_node_affinity (PATCH /VirDomain with clean fields).


Suggested fix

Since v1 silently ignores extra fields, stripping these from PATCH is safe for both v1 and v2 — no version detection needed.

The pattern for each fix is to make the parent-reference fields conditional on whether the call is a create (POST) or update (PATCH). Example for nic.py:

def to_hypercore(self, for_update=False):
payload = {
"vlan": self.vlan,
"connected": self.connected,
"type": self.type.value,
"macAddress": self.mac,
}
if not for_update:
payload["virDomainUUID"] = self.vm_uuid
return payload

Same pattern for replication.py (sourceDomainUUID) and disk.py (virDomainUUID, slot). Update call sites that perform PATCH to pass for_update=True.


Context

  • HyperCore /rest/v1 is planned to be renamed/replaced by /rest/v2 as the public API
  • v2 uses additionalProperties: false on all PATCH schemas — any field not explicitly in the schema causes a hard 400 rejection (not a silent no-op as in v1)
  • The v2 API team is also aware of these missing fields and is planning to restore them in v2 before public release; this collection fix provides defence-in-depth so the collection works correctly regardless of API version
  • These three fields are the only collection → v2 incompatibilities found; all other collection operations (55/69 integration test targets) work correctly against v2/9.7.1

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions