0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-06-30 02:31:10 +00:00

feat(cluster): Implement initial support for "hardware mappings" (#1213)

* feat(cluster): Implement initial support for "hardware mappings"

Right now it is alredy possible to use a mapped resource [1], but there
is no dedicated `proxmox_virtual_environment_cluster_hardware_mapping`
resource but this step must still be done manually (or automated through
other ways that interact with the Proxmox API).

This commit implements support for "hardware mapping" resources and data
sources for the, currently, available bus types PCI and USB, based on
the Proxmox VE API documentations [2].

There are some "specialities" in these resources and data sources:

1. The Proxmox VE API attribute, but this implementations names it
   "comment" since this naming is generally across the Proxmox VE web UI
   and API documentations. This still follows the Terraform
   "best practices" [3] as it improves the user experience by matching
   the field name to the naming used in the human-facing interfaces.

2. Like in point 1, the name of the attribute of "node checks
   diagnostics" for USB hardware mappings is "errors" in the Proxmox VE
   API while it is "checks" for hardware mappings of type PCI.
   The second naming pattern is also generally used across the
   Proxmox VE web UI and API documentations, including the "check_node"
   attribute that is also implemented in the
   "proxmox_virtual_environment_hardware_mappings" data source.
   Therefore, this implementation named both attributes "checks" which
   still follows the Terraform "best practices" [3] as it improves the
   user experience by matching the field name to the naming used in the
   human-facing interfaces.
3. This implmenetation comes with the "unique" feature of allowing
   comments (named "descriptions" by the Proxmox VE API) for an entry in
   a device map which is not possible through the web UI at all but only
   adding a comment for the whole mapping entry instead.

Note that this implementation also adds another point in the
"Known Issues" documentation since it is only possible to map a
PCI/USB device using the `root` PAM account, but this is still better
than having to manually configure it through the web UI or by
interacting with the Proxmox VE API on other ways.

[1]: https://github.com/bpg/terraform-provider-proxmox/pull/500
[2]: https://pve.proxmox.com/pve-docs/api-viewer/#/cluster/mapping/pci
[3]: https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles#resource-and-attribute-schema-should-closely-match-the-underlying-api

Signed-off-by: Sven Greb <development@svengreb.de>

* fix linter

Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>

---------

Signed-off-by: Sven Greb <development@svengreb.de>
Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
Sven Greb 2024-04-18 22:00:51 +02:00 committed by GitHub
parent 163a773088
commit 76c1c2282b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 3820 additions and 0 deletions

View File

@ -26,6 +26,10 @@ issues:
- path: fwprovider/tests/.*\.go
linters:
- paralleltest
# Exclude `lll` issues for long lines with URLs.
- linters:
- lll
source: "^.*https?://.*$"
linters-settings:
exhaustive:
default-signifies-exhaustive: true

View File

@ -116,6 +116,11 @@ resource "proxmox_virtual_environment_vm" "example" {
Due to limitations in the Proxmox VE API, certain files (snippets, backups) need to be uploaded using SFTP.
This requires the use of a PAM account (standard Linux account).
### Cluster hardware mappings cannot be created by non-PAM accounts
Due to limitations in the Proxmox VE API, cluster hardware mappings must be created using the `root` PAM account (standard Linux account) due to [IOMMU](https://en.wikipedia.org/wiki/Input%E2%80%93output_memory_management_unit#Virtualization) interactions.
Hardware mappings allow to use [PCI "passthrough"](https://pve.proxmox.com/wiki/PCI_Passthrough) and [map physical USB ports](https://pve.proxmox.com/wiki/USB_Physical_Port_Mapping).
## Contributors
See [CONTRIBUTORS.md](CONTRIBUTORS.md) for a list of contributors to this project.

View File

@ -0,0 +1,50 @@
---
layout: page
title: proxmox_virtual_environment_hardware_mapping_pci
parent: Data Sources
subcategory: Virtual Environment
description: |-
Retrieves a PCI hardware mapping from a Proxmox VE cluster.
---
# Data Source: proxmox_virtual_environment_hardware_mapping_pci
Retrieves a PCI hardware mapping from a Proxmox VE cluster.
## Example Usage
```terraform
data "proxmox_virtual_environment_hardware_mapping_pci" "example" {
name = "example"
}
output "data_proxmox_virtual_environment_hardware_mapping_pci" {
value = data.proxmox_virtual_environment_hardware_mapping_pci.example
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `name` (String) The name of this PCI hardware mapping.
### Read-Only
- `comment` (String) The comment of this PCI hardware mapping.
- `id` (String) The unique identifier of this PCI hardware mapping data source.
- `map` (Attributes Set) The actual map of devices for the hardware mapping. (see [below for nested schema](#nestedatt--map))
- `mediated_devices` (Boolean) Indicates whether to use with mediated devices.
<a id="nestedatt--map"></a>
### Nested Schema for `map`
Read-Only:
- `comment` (String) The comment of the mapped PCI device.
- `id` (String) The ID attribute of the map.
- `iommu_group` (Number) The IOMMU group attribute of the map.
- `node` (String) The node name attribute of the map.
- `path` (String) The path attribute of the map.
- `subsystem_id` (String) The subsystem ID attribute of the map.Not mandatory for the Proxmox VE API call, but causes a PCI hardware mapping to be incomplete when not set.

View File

@ -0,0 +1,47 @@
---
layout: page
title: proxmox_virtual_environment_hardware_mapping_usb
parent: Data Sources
subcategory: Virtual Environment
description: |-
Retrieves a USB hardware mapping from a Proxmox VE cluster.
---
# Data Source: proxmox_virtual_environment_hardware_mapping_usb
Retrieves a USB hardware mapping from a Proxmox VE cluster.
## Example Usage
```terraform
data "proxmox_virtual_environment_hardware_mapping_usb" "example" {
name = "example"
}
output "data_proxmox_virtual_environment_hardware_mapping_usb" {
value = data.proxmox_virtual_environment_hardware_mapping_usb.example
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `name` (String) The name of this USB hardware mapping.
### Read-Only
- `comment` (String) The comment of this USB hardware mapping.
- `id` (String) The unique identifier of this USB hardware mapping data source.
- `map` (Attributes Set) The actual map of devices for the hardware mapping. (see [below for nested schema](#nestedatt--map))
<a id="nestedatt--map"></a>
### Nested Schema for `map`
Read-Only:
- `comment` (String) The comment of the mapped USB device.
- `id` (String) The ID attribute of the map.
- `node` (String) The node name attribute of the map.
- `path` (String) The path attribute of the map.

View File

@ -0,0 +1,60 @@
---
layout: page
title: proxmox_virtual_environment_hardware_mappings
parent: Data Sources
subcategory: Virtual Environment
description: |-
Retrieves a list of hardware mapping resources.
---
# Data Source: proxmox_virtual_environment_hardware_mappings
Retrieves a list of hardware mapping resources.
## Example Usage
```terraform
data "proxmox_virtual_environment_hardware_mappings" "example-pci" {
check_node = "pve"
type = "pci"
}
data "proxmox_virtual_environment_hardware_mappings" "example-usb" {
check_node = "pve"
type = "usb"
}
output "data_proxmox_virtual_environment_hardware_mappings_pci" {
value = data.proxmox_virtual_environment_hardware_mappings.example-pci
}
output "data_proxmox_virtual_environment_hardware_mappings_usb" {
value = data.proxmox_virtual_environment_hardware_mappings.example-usb
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `type` (String) The type of the hardware mappings.
### Optional
- `check_node` (String) The name of the node whose configurations should be checked for correctness.
### Read-Only
- `checks` (Attributes List) Might contain relevant diagnostics about incorrect configurations. (see [below for nested schema](#nestedatt--checks))
- `id` (String) The unique identifier of this hardware mappings data source.
- `ids` (Set of String) The identifiers of the hardware mappings.
<a id="nestedatt--checks"></a>
### Nested Schema for `checks`
Read-Only:
- `mapping_id` (String) The corresponding hardware mapping ID of the node check diagnostic entry.
- `message` (String) The message of the node check diagnostic entry.
- `severity` (String) The severity of the node check diagnostic entry.

View File

@ -0,0 +1,77 @@
---
layout: page
title: proxmox_virtual_environment_hardware_mapping_pci
parent: Resources
subcategory: Virtual Environment
description: |-
Manages a PCI hardware mapping in a Proxmox VE cluster.
---
# Resource: proxmox_virtual_environment_hardware_mapping_pci
Manages a PCI hardware mapping in a Proxmox VE cluster.
## Example Usage
```terraform
resource "proxmox_virtual_environment_hardware_mapping_pci" "example" {
comment = "This is a comment"
name = "example"
# The actual map of devices.
map = [
{
comment = "This is a device specific comment"
id = "8086:5916"
# This is an optional attribute, but causes a mapping to be incomplete when not defined.
iommu_group = 0
node = "pve"
path = "0000:00:02.0"
# This is an optional attribute, but causes a mapping to be incomplete when not defined.
subsystem_id = "8086:2068"
},
]
mediated_devices = true
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `map` (Attributes Set) The actual map of devices for the PCI hardware mapping. (see [below for nested schema](#nestedatt--map))
- `name` (String) The name of this PCI hardware mapping.
### Optional
- `comment` (String) The comment of this PCI hardware mapping.
- `mediated_devices` (Boolean) Indicates whether to enable mediated devices.
### Read-Only
- `id` (String) The unique identifier of this PCI hardware mapping resource.
<a id="nestedatt--map"></a>
### Nested Schema for `map`
Required:
- `id` (String) The ID of the map.
- `node` (String) The node name of the map.
- `path` (String) The path of the map.
Optional:
- `comment` (String) The comment of the mapped PCI device.
- `iommu_group` (Number) The IOMMU group of the map. Not mandatory for the Proxmox VE API call, but causes a PCI hardware mapping to be incomplete when not set
- `subsystem_id` (String) The subsystem ID group of the map. Not mandatory for the Proxmox VE API call, but causes a PCI hardware mapping to be incomplete when not set
## Import
Import is supported using the following syntax:
```shell
#!/usr/bin/env sh
# A PCI hardware mapping can be imported using their name, e.g.:
terraform import proxmox_virtual_environment_hardware_mapping_pci.example example
```

View File

@ -0,0 +1,70 @@
---
layout: page
title: proxmox_virtual_environment_hardware_mapping_usb
parent: Resources
subcategory: Virtual Environment
description: |-
Manages a USB hardware mapping in a Proxmox VE cluster.
---
# Resource: proxmox_virtual_environment_hardware_mapping_usb
Manages a USB hardware mapping in a Proxmox VE cluster.
## Example Usage
```terraform
resource "proxmox_virtual_environment_hardware_mapping_usb" "example" {
comment = "This is a comment"
name = "example"
# The actual map of devices.
map = [
{
comment = "This is a device specific comment"
id = "8087:0a2b"
node = "pve"
# This attribute is optional, but can be used to map the device based on its port instead of only the device ID.
path = "1-8.2"
},
]
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `map` (Attributes Set) The actual map of devices for the hardware mapping. (see [below for nested schema](#nestedatt--map))
- `name` (String) The name of this hardware mapping.
### Optional
- `comment` (String) The comment of this USB hardware mapping.
### Read-Only
- `id` (String) The unique identifier of this USB hardware mapping resource.
<a id="nestedatt--map"></a>
### Nested Schema for `map`
Required:
- `id` (String) The ID of the map.
- `node` (String) The node name of the map.
Optional:
- `comment` (String) The comment of the mapped USB device.
- `path` (String) The path of the map. For hardware mappings of type USB the path is optional and indicates that the device is mapped through the device ID instead of ports.
## Import
Import is supported using the following syntax:
```shell
#!/usr/bin/env sh
# A USB hardware mapping can be imported using their name, e.g.:
terraform import proxmox_virtual_environment_hardware_mapping_usb.example example
```

View File

@ -0,0 +1,7 @@
data "proxmox_virtual_environment_hardware_mapping_pci" "example" {
name = "example"
}
output "data_proxmox_virtual_environment_hardware_mapping_pci" {
value = data.proxmox_virtual_environment_hardware_mapping_pci.example
}

View File

@ -0,0 +1,7 @@
data "proxmox_virtual_environment_hardware_mapping_usb" "example" {
name = "example"
}
output "data_proxmox_virtual_environment_hardware_mapping_usb" {
value = data.proxmox_virtual_environment_hardware_mapping_usb.example
}

View File

@ -0,0 +1,17 @@
data "proxmox_virtual_environment_hardware_mappings" "example-pci" {
check_node = "pve"
type = "pci"
}
data "proxmox_virtual_environment_hardware_mappings" "example-usb" {
check_node = "pve"
type = "usb"
}
output "data_proxmox_virtual_environment_hardware_mappings_pci" {
value = data.proxmox_virtual_environment_hardware_mappings.example-pci
}
output "data_proxmox_virtual_environment_hardware_mappings_usb" {
value = data.proxmox_virtual_environment_hardware_mappings.example-usb
}

View File

@ -0,0 +1,3 @@
#!/usr/bin/env sh
# A PCI hardware mapping can be imported using their name, e.g.:
terraform import proxmox_virtual_environment_hardware_mapping_pci.example example

View File

@ -0,0 +1,18 @@
resource "proxmox_virtual_environment_hardware_mapping_pci" "example" {
comment = "This is a comment"
name = "example"
# The actual map of devices.
map = [
{
comment = "This is a device specific comment"
id = "8086:5916"
# This is an optional attribute, but causes a mapping to be incomplete when not defined.
iommu_group = 0
node = "pve"
path = "0000:00:02.0"
# This is an optional attribute, but causes a mapping to be incomplete when not defined.
subsystem_id = "8086:2068"
},
]
mediated_devices = true
}

View File

@ -0,0 +1,3 @@
#!/usr/bin/env sh
# A USB hardware mapping can be imported using their name, e.g.:
terraform import proxmox_virtual_environment_hardware_mapping_usb.example example

View File

@ -0,0 +1,14 @@
resource "proxmox_virtual_environment_hardware_mapping_usb" "example" {
comment = "This is a comment"
name = "example"
# The actual map of devices.
map = [
{
comment = "This is a device specific comment"
id = "8087:0a2b"
node = "pve"
# This attribute is optional, but can be used to map the device based on its port instead of only the device ID.
path = "1-8.2"
},
]
}

View File

@ -0,0 +1,205 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package hardwaremapping
import (
"context"
"fmt"
"slices"
"strings"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/bpg/terraform-provider-proxmox/fwprovider/structure"
"github.com/bpg/terraform-provider-proxmox/proxmox"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/mapping"
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/hardwaremapping"
)
// Ensure the implementation satisfies the required interfaces.
var (
_ datasource.DataSource = &dataSource{}
_ datasource.DataSourceWithConfigure = &dataSource{}
)
// dataSource is the data source implementation for a hardware mapping.
type dataSource struct {
client *mapping.Client
}
// Configure adds the provider-configured client to the data source.
func (d *dataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(proxmox.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *proxmox.Client, got: %T", req.ProviderData),
)
return
}
d.client = client.Cluster().HardwareMapping()
}
// Metadata returns the data source type name.
func (d *dataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_hardware_mappings"
}
// Read fetches the list of hardware mappings from the Proxmox VE API then converts it to a list of strings.
func (d *dataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var hm model
resp.Diagnostics.Append(req.Config.Get(ctx, &hm)...)
if resp.Diagnostics.HasError() {
return
}
hmType, err := proxmoxtypes.ParseType(hm.Type.ValueString())
if err != nil {
resp.Diagnostics.AddError("Could not parse hardware mapping type", err.Error())
return
}
list, err := d.client.List(ctx, hmType, hm.CheckNode.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Unable to read hardware mappings",
err.Error(),
)
return
}
createCheckDiagnostics := func(hmID string, input []mapping.NodeCheckDiag) []modelNodeCheckDiag {
checks := make([]modelNodeCheckDiag, len(input))
for idx, check := range input {
m := modelNodeCheckDiag{
MappingID: types.StringValue(hmID),
Severity: types.StringPointerValue(check.Severity),
}
// Strip the unnecessary new line control character (\n) from the end of the message that is, for whatever reason,
// returned this way by the Proxmox VE API.
msg := strings.TrimSuffix(types.StringPointerValue(check.Message).ValueString(), "\n")
m.Message = types.StringPointerValue(&msg)
checks[idx] = m
}
return checks
}
mappings := make([]attr.Value, len(list))
for idx, data := range list {
mappings[idx] = types.StringValue(data.ID)
// One of the fields only exists when the "check-node" option was passed to the Proxmox VE API with a valid node
// name.
// Note that the Proxmox VE API, for whatever reason, only returns one error at a time, even though the field is an
// array.
if (data.ChecksPCI != nil && len(data.ChecksPCI) > 0) || (data.ChecksUSB != nil && len(data.ChecksUSB) > 0) {
switch data.Type {
case proxmoxtypes.TypePCI:
hm.Checks = append(hm.Checks, createCheckDiagnostics(data.ID, data.ChecksPCI)...)
case proxmoxtypes.TypeUSB:
hm.Checks = append(hm.Checks, createCheckDiagnostics(data.ID, data.ChecksUSB)...)
}
}
// Ensure to keep the order of the diagnostic entries to prevent random plan changes.
slices.SortStableFunc(
hm.Checks, func(a, b modelNodeCheckDiag) int {
return strings.Compare(a.MappingID.ValueString(), b.MappingID.ValueString())
},
)
}
values, diags := types.SetValue(types.StringType, mappings)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
hm.MappingIDs = values
hm.ID = types.StringValue("hardware_mappings")
resp.Diagnostics.Append(resp.State.Set(ctx, &hm)...)
}
// Schema returns the schema for the data source.
func (d *dataSource) Schema(
_ context.Context,
_ datasource.SchemaRequest,
resp *datasource.SchemaResponse,
) {
resp.Schema = schema.Schema{
Description: "Retrieves a list of hardware mapping resources.",
Attributes: map[string]schema.Attribute{
schemaAttrNameChecks: schema.ListNestedAttribute{
Computed: true,
Description: `Might contain relevant diagnostics about incorrect configurations.`,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
schemaAttrNameChecksDiagsMappingID: schema.StringAttribute{
Computed: true,
Description: "The corresponding hardware mapping ID of the node check diagnostic entry.",
},
schemaAttrNameChecksDiagsMessage: schema.StringAttribute{
Computed: true,
Description: "The message of the node check diagnostic entry.",
},
schemaAttrNameChecksDiagsSeverity: schema.StringAttribute{
Computed: true,
Description: "The severity of the node check diagnostic entry.",
},
},
},
},
schemaAttrNameCheckNode: schema.StringAttribute{
Description: "The name of the node whose configurations should be checked for correctness.",
Optional: true,
},
schemaAttrNameHWMIDs: schema.SetAttribute{
ElementType: types.StringType,
Computed: true,
Description: "The identifiers of the hardware mappings.",
},
schemaAttrNameTerraformID: structure.IDAttribute(
"The unique identifier of this hardware mappings data source.",
),
schemaAttrNameType: schema.StringAttribute{
Description: "The type of the hardware mappings.",
Required: true,
Validators: []validator.String{
stringvalidator.OneOf(
[]string{
proxmoxtypes.TypePCI.String(),
proxmoxtypes.TypeUSB.String(),
}...,
),
},
},
},
}
}
// NewDataSource returns a new data source for hardware mappings.
// This is a helper function to simplify the provider implementation.
func NewDataSource() datasource.DataSource {
return &dataSource{}
}

View File

@ -0,0 +1,169 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package hardwaremapping
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/bpg/terraform-provider-proxmox/fwprovider/structure"
customtypes "github.com/bpg/terraform-provider-proxmox/fwprovider/types/hardwaremapping"
"github.com/bpg/terraform-provider-proxmox/fwprovider/validators"
"github.com/bpg/terraform-provider-proxmox/proxmox"
mappings "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/mapping"
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/hardwaremapping"
)
// Ensure the implementation satisfies the required interfaces.
var (
_ datasource.DataSource = &dataSourcePCI{}
_ datasource.DataSourceWithConfigure = &dataSourcePCI{}
)
// dataSourcePCI is the data source implementation for a PCI hardware mapping.
type dataSourcePCI struct {
// client is the hardware mapping API client.
client *mappings.Client
}
// Configure adds the provider-configured client to the data source.
func (d *dataSourcePCI) Configure(
_ context.Context,
req datasource.ConfigureRequest,
resp *datasource.ConfigureResponse,
) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(proxmox.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *proxmox.Client, got: %T", req.ProviderData),
)
return
}
d.client = client.Cluster().HardwareMapping()
}
// Metadata returns the data source type name.
func (d *dataSourcePCI) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_hardware_mapping_pci"
}
// Read fetches the specified PCI hardware mapping from the Proxmox VE API.
func (d *dataSourcePCI) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var hm modelPCI
resp.Diagnostics.Append(req.Config.Get(ctx, &hm)...)
if resp.Diagnostics.HasError() {
return
}
hmID := hm.Name.ValueString()
// Ensure to keep both in sync since the name represents the ID.
hm.ID = hm.Name
data, err := d.client.Get(ctx, proxmoxtypes.TypePCI, hmID)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Unable to read PCI hardware mapping %q", hmID),
err.Error(),
)
return
}
hm.importFromAPI(ctx, data)
resp.Diagnostics.Append(resp.State.Set(ctx, &hm)...)
}
// Schema defines the schema for the PCI hardware mapping.
func (d *dataSourcePCI) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
comment := dataSourceSchemaBaseAttrComment
comment.Optional = false
comment.Computed = true
comment.Description = "The comment of this PCI hardware mapping."
commentMap := comment
commentMap.Description = "The comment of the mapped PCI device."
resp.Schema = schema.Schema{
Description: "Retrieves a PCI hardware mapping from a Proxmox VE cluster.",
Attributes: map[string]schema.Attribute{
schemaAttrNameComment: comment,
schemaAttrNameMap: schema.SetNestedAttribute{
Computed: true,
Description: "The actual map of devices for the hardware mapping.",
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
schemaAttrNameComment: commentMap,
schemaAttrNameMapIOMMUGroup: schema.Int64Attribute{
Computed: true,
Description: "The IOMMU group attribute of the map.",
},
schemaAttrNameMapDeviceID: schema.StringAttribute{
Computed: true,
Description: "The ID attribute of the map.",
Validators: []validator.String{
validators.HardwareMappingDeviceIDValidator(),
},
},
schemaAttrNameMapNode: schema.StringAttribute{
Computed: true,
Description: "The node name attribute of the map.",
},
schemaAttrNameMapPath: schema.StringAttribute{
// For hardware mappings of type PCI, the path is required while it is optional for USB.
Computed: true,
CustomType: customtypes.PathType{},
Description: "The path attribute of the map.",
},
schemaAttrNameMapSubsystemID: schema.StringAttribute{
Computed: true,
Description: "The subsystem ID attribute of the map." +
"Not mandatory for the Proxmox VE API call, but causes a PCI hardware mapping to be incomplete when " +
"not set.",
Validators: []validator.String{
validators.HardwareMappingDeviceIDValidator(),
},
},
},
},
Validators: []validator.Set{
setvalidator.SizeAtLeast(1),
},
},
schemaAttrNameMediatedDevices: schema.BoolAttribute{
Computed: true,
Description: "Indicates whether to use with mediated devices.",
},
schemaAttrNameName: schema.StringAttribute{
Description: "The name of this PCI hardware mapping.",
Required: true,
},
schemaAttrNameTerraformID: structure.IDAttribute(
"The unique identifier of this PCI hardware mapping data source.",
),
},
}
}
// NewDataSourcePCI returns a new data source for a PCI hardware mapping.
// This is a helper function to simplify the provider implementation.
func NewDataSourcePCI() datasource.DataSource {
return &dataSourcePCI{}
}

View File

@ -0,0 +1,151 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package hardwaremapping
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/bpg/terraform-provider-proxmox/fwprovider/structure"
customtypes "github.com/bpg/terraform-provider-proxmox/fwprovider/types/hardwaremapping"
"github.com/bpg/terraform-provider-proxmox/fwprovider/validators"
"github.com/bpg/terraform-provider-proxmox/proxmox"
mappings "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/mapping"
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/hardwaremapping"
)
// Ensure the implementation satisfies the required interfaces.
var (
_ datasource.DataSource = &datasourceUSB{}
_ datasource.DataSourceWithConfigure = &datasourceUSB{}
)
// datasourceUSB is the data source implementation for a USB hardware mapping.
type datasourceUSB struct {
client *mappings.Client
}
// Configure adds the provider-configured client to the data source.
func (d *datasourceUSB) Configure(
_ context.Context,
req datasource.ConfigureRequest,
resp *datasource.ConfigureResponse,
) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(proxmox.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *proxmox.Client, got: %T", req.ProviderData),
)
return
}
d.client = client.Cluster().HardwareMapping()
}
// Metadata returns the data source type name.
func (d *datasourceUSB) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_hardware_mapping_usb"
}
// Read fetches the specified USB hardware mapping from the Proxmox VE API.
func (d *datasourceUSB) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var hm modelUSB
resp.Diagnostics.Append(req.Config.Get(ctx, &hm)...)
if resp.Diagnostics.HasError() {
return
}
hmID := hm.Name.ValueString()
// Ensure to keep both in sync since the name represents the ID.
hm.ID = hm.Name
data, err := d.client.Get(ctx, proxmoxtypes.TypeUSB, hmID)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Unable to read USB hardware mapping %q", hmID),
err.Error(),
)
return
}
hm.importFromAPI(ctx, data)
resp.Diagnostics.Append(resp.State.Set(ctx, &hm)...)
}
// Schema defines the schema for the USB hardware mapping.
func (d *datasourceUSB) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
comment := dataSourceSchemaBaseAttrComment
comment.Optional = false
comment.Computed = true
comment.Description = "The comment of this USB hardware mapping."
commentMap := comment
commentMap.Description = "The comment of the mapped USB device."
resp.Schema = schema.Schema{
Description: "Retrieves a USB hardware mapping from a Proxmox VE cluster.",
Attributes: map[string]schema.Attribute{
schemaAttrNameComment: comment,
schemaAttrNameMap: schema.SetNestedAttribute{
Computed: true,
Description: "The actual map of devices for the hardware mapping.",
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
schemaAttrNameComment: commentMap,
schemaAttrNameMapDeviceID: schema.StringAttribute{
Computed: true,
Description: "The ID attribute of the map.",
Validators: []validator.String{
validators.HardwareMappingDeviceIDValidator(),
},
},
schemaAttrNameMapNode: schema.StringAttribute{
Computed: true,
Description: "The node name attribute of the map.",
},
schemaAttrNameMapPath: schema.StringAttribute{
// For hardware mappings of type USB the path is optional and indicates that the device is mapped through
// the device ID instead of ports.
Computed: true,
CustomType: customtypes.PathType{},
Description: "The path attribute of the map.",
},
},
},
Validators: []validator.Set{
setvalidator.SizeAtLeast(1),
},
},
schemaAttrNameName: schema.StringAttribute{
Description: "The name of this USB hardware mapping.",
Required: true,
},
schemaAttrNameTerraformID: structure.IDAttribute(
"The unique identifier of this USB hardware mapping data source.",
),
},
}
}
// NewDataSourceUSB returns a new data source for a USB hardware mapping.
// This is a helper function to simplify the provider implementation.
func NewDataSourceUSB() datasource.DataSource {
return &datasourceUSB{}
}

View File

@ -0,0 +1,22 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package hardwaremapping
import (
"fmt"
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/hardwaremapping"
)
// ErrResourceMessageInvalidPath is the error message for an invalid Linux device path for a hardware mapping of the
// specified type.
// Extracting the message helps to reduce duplicated code and allows to use it in automated unit and acceptance tests.
//
//nolint:gochecknoglobals
var ErrResourceMessageInvalidPath = func(hmType proxmoxtypes.Type) string {
return fmt.Sprintf("not a valid Linux device path for hardware mapping of type %q", hmType)
}

View File

@ -0,0 +1,406 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package hardwaremapping
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/types"
customtypes "github.com/bpg/terraform-provider-proxmox/fwprovider/types/hardwaremapping"
apitypes "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/mapping"
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/hardwaremapping"
)
const (
// schemaAttrNameComment is the name of the schema attribute for the comment of a hardware mapping.
// Note that the Proxmox VE API attribute is named "description", but we map it as a comment since this naming is
// generally across the Proxmox VE web UI and API documentations. This still follows the [Terraform "best practices"]
// as it improves the user experience by matching the field name to the naming used in the human-facing interfaces.
//
// [Terraform "best practices"]: https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles#resource-and-attribute-schema-should-closely-match-the-underlying-api
schemaAttrNameComment = "comment"
// schemaAttrNameMap is the name of the schema attribute for the map of a hardware mapping.
schemaAttrNameMap = "map"
// schemaAttrNameMapDeviceID is the name of the schema attribute for the device ID in a map of a hardware mapping.
schemaAttrNameMapDeviceID = "id"
// schemaAttrNameMapIOMMUGroup is the name of the schema attribute for the IOMMU group in a map of a hardware mapping.
schemaAttrNameMapIOMMUGroup = "iommu_group"
// schemaAttrNameMapNode is the name of the schema attribute for the node in a map of a hardware mapping.
schemaAttrNameMapNode = "node"
// schemaAttrNameMapPath is the name of the schema attribute for the path in a map of a hardware mapping.
schemaAttrNameMapPath = "path"
// schemaAttrNameMapSubsystemID is the name of the schema attribute for the subsystem ID in a map of a hardware
// mapping.
schemaAttrNameMapSubsystemID = "subsystem_id"
// schemaAttrNameMediatedDevices is the name of the schema attribute for the mediated devices in a map of a hardware
// mapping.
schemaAttrNameMediatedDevices = "mediated_devices"
// schemaAttrNameName is the name of the schema attribute for the name of a hardware mapping.
schemaAttrNameName = "name"
// schemaAttrNameTerraformID is the name of the schema attribute for the Terraform ID of a hardware mapping.
schemaAttrNameTerraformID = "id"
// schemaAttrNameType is the name of the schema attribute for the [proxmoxtypes.Type].
schemaAttrNameType = "type"
// schemaAttrNameCheckNode is the name of the schema attribute for the "check node" option of a
// dataSource.
schemaAttrNameCheckNode = "check_node"
// schemaAttrNameChecks is the name of the schema attribute for the node checks diagnostics of a hardware mapping data
// source.
// Note that the Proxmox VE API attribute for [proxmoxtypes.TypeUSB] is named "errors", but we map it as "checks"
// since this naming is generally across the Proxmox VE web UI and API documentations, including the attribute for
// [proxmoxtypes.TypePCI].
// This still follows the [Terraform "best practices"] as it improves the user experience by matching the field name
// to the naming used in the human-facing interfaces.
//
// [Terraform "best practices"]: https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles#resource-and-attribute-schema-should-closely-match-the-underlying-api
schemaAttrNameChecks = "checks"
// schemaAttrNameChecksDiagsMappingID is the name of the schema attribute for a node check diagnostic mapping ID of a
// dataSource.
schemaAttrNameChecksDiagsMappingID = "mapping_id"
// schemaAttrNameChecksDiagsMessage is the name of the schema attribute for a node check diagnostic message of a
// dataSource.
schemaAttrNameChecksDiagsMessage = "message"
// schemaAttrNameChecksDiagsSeverity is the name of the schema attribute for a node check diagnostic severity of a
// dataSource.
schemaAttrNameChecksDiagsSeverity = "severity"
// schemaAttrNameHWMIDs is the name of the schema attribute for the hardware mapping IDs of a
// dataSource.
schemaAttrNameHWMIDs = "ids"
)
// modelPCIMap maps the schema data for the map of a PCI hardware mapping.
type modelPCIMap struct {
// Comment is the "comment" for the map.
// This field is optional and is omitted by the Proxmox VE API when not set.
// Note that the Proxmox VE API attribute is named "description", but we map it as a comment since this naming is
// generally across the Proxmox VE web UI and API documentations. This still follows the [Terraform "best practices"]
// as it improves the user experience by matching the field name to the naming used in the human-facing interfaces.
//
// [Terraform "best practices"]: https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles#resource-and-attribute-schema-should-closely-match-the-underlying-api
Comment types.String `tfsdk:"comment"`
// ID is the identifier of the map.
ID types.String `tfsdk:"id"`
// IOMMUGroup is the "IOMMU group" for the map.
// This field is optional and is omitted by the Proxmox VE API when not set.
IOMMUGroup types.Int64 `tfsdk:"iommu_group"`
// Node is the "node name" for the map.
Node types.String `tfsdk:"node"`
// Path is the "path" for the map.
Path customtypes.PathValue `tfsdk:"path"`
// SubsystemID is the "subsystem ID" for the map.
// This field is not mandatory for the Proxmox VE API call, but causes a PCI hardware mapping to be incomplete when
// not set.
SubsystemID types.String `tfsdk:"subsystem_id"`
}
// modelUSBMap maps the schema data for the map of a USB hardware mapping.
type modelUSBMap struct {
// Comment is the "comment" for the map.
// This field is optional and is omitted by the Proxmox VE API when not set.
// Note that the Proxmox VE API attribute is named "description", but we map it as a comment since this naming is
// generally across the Proxmox VE web UI and API documentations. This still follows the [Terraform "best practices"]
// as it improves the user experience by matching the field name to the naming used in the human-facing interfaces.
//
// [Terraform "best practices"]: https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles#resource-and-attribute-schema-should-closely-match-the-underlying-api
Comment types.String `tfsdk:"comment"`
// ID is the identifier of the map.
ID types.String `tfsdk:"id"`
// Node is the "node name" for the map.
Node types.String `tfsdk:"node"`
// Path is the "path" for the map.
Path customtypes.PathValue `tfsdk:"path"`
}
// modelPCI maps the schema data for a PCI hardware mapping.
type modelPCI struct {
// Comment is the comment of the PCI hardware mapping.
// Note that the Proxmox VE API attribute is named "description", but we map it as a comment since this naming is
// generally across the Proxmox VE web UI and API documentations. This still follows the [Terraform "best practices"]
// as it improves the user experience by matching the field name to the naming used in the human-facing interfaces.
//
// [Terraform "best practices"]: https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles#resource-and-attribute-schema-should-closely-match-the-underlying-api
Comment types.String `tfsdk:"comment"`
// ID is the Terraform identifier.
ID types.String `tfsdk:"id"`
// Name is the name of the PCI hardware mapping.
Name types.String `tfsdk:"name"`
// Map is the map of the PCI hardware mapping.
Map []modelPCIMap `tfsdk:"map"`
// MediatedDevices is the indicator for mediated devices of the PCI hardware mapping.
MediatedDevices types.Bool `tfsdk:"mediated_devices"`
}
// modelUSB maps the schema data for a USB hardware mapping.
type modelUSB struct {
// Comment is the comment of the USB hardware mapping.
// Note that the Proxmox VE API attribute is named "description", but we map it as a comment since this naming is
// generally across the Proxmox VE web UI and API documentations. This still follows the [Terraform "best practices"]
// as it improves the user experience by matching the field name to the naming used in the human-facing interfaces.
//
// [Terraform "best practices"]: https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles#resource-and-attribute-schema-should-closely-match-the-underlying-api
Comment types.String `tfsdk:"comment"`
// ID is the Terraform identifier.
ID types.String `tfsdk:"id"`
// Name is the name of the USB hardware mapping.
Name types.String `tfsdk:"name"`
// Map is the map of the USB hardware mapping.
Map []modelUSBMap `tfsdk:"map"`
}
// model maps the schema data for a hardware mappings data source.
type model struct {
// Checks might contain relevant hardware mapping diagnostics about incorrect configurations for the node name set
// defined by CheckNode.
// Note that the Proxmox VE API attribute for [proxmoxtypes.TypeUSB] is named "errors", but we map it as "checks"
// since this naming is generally across the Proxmox VE web UI and API documentations, including the attribute for
// [proxmoxtypes.TypePCI].
// Also note that the Proxmox VE API, for whatever reason, only returns one error at a time, even though the field is
// an array.
// This still follows the [Terraform "best practices"] as it improves the user experience by matching the field name
// to the naming used in the human-facing interfaces.
//
// [Terraform "best practices"]: https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles#resource-and-attribute-schema-should-closely-match-the-underlying-api
Checks []modelNodeCheckDiag `tfsdk:"checks"`
// CheckNode is the name of the node whose configuration should be checked for correctness.
CheckNode types.String `tfsdk:"check_node"`
// ID is the Terraform identifier.
ID types.String `tfsdk:"id"`
// MappingIDs is the set of hardware mapping identifiers.
MappingIDs types.Set `tfsdk:"ids"`
// Type is the [proxmoxtypes.Type].
Type types.String `tfsdk:"type"`
}
// modelNodeCheckDiag maps the schema data for hardware mapping node check diagnostic data.
type modelNodeCheckDiag struct {
// MappingID is the corresponding hardware mapping ID of this node check diagnostic entry.
MappingID types.String `tfsdk:"mapping_id"`
// Message is the message of the node check diagnostic entry.
Message types.String `tfsdk:"message"`
// Severity is the severity of the node check diagnostic entry.
Severity types.String `tfsdk:"severity"`
}
// importFromAPI imports the contents of a PCI hardware mapping model from the Proxmox VE API's response data.
func (hm *modelPCI) importFromAPI(_ context.Context, data *apitypes.GetResponseData) {
// Ensure that both the ID and name are in sync.
hm.Name = hm.ID
// The attribute is named "description" by the Proxmox VE API, but we map it as a comment since this naming is
// generally across the Proxmox VE web UI and API documentations.
hm.Comment = types.StringPointerValue(data.Description)
maps := make([]modelPCIMap, len(data.Map))
for idx, pveMap := range data.Map {
tfMap := modelPCIMap{
ID: pveMap.ID.ToValue(),
Node: types.StringValue(pveMap.Node),
Path: customtypes.NewPathPointerValue(pveMap.Path),
}
if pveMap.Description != nil {
tfMap.Comment = types.StringPointerValue(pveMap.Description)
}
if pveMap.SubsystemID != "" {
tfMap.SubsystemID = pveMap.SubsystemID.ToValue()
}
if pveMap.IOMMUGroup != nil {
tfMap.IOMMUGroup = types.Int64Value(*pveMap.IOMMUGroup)
}
maps[idx] = tfMap
}
hm.MediatedDevices = data.MediatedDevices.ToValue()
hm.Map = maps
}
// toCreateRequest builds the request data structure for creating a new PCI hardware mapping.
func (hm *modelPCI) toCreateRequest() *apitypes.CreateRequestBody {
return &apitypes.CreateRequestBody{
DataBase: hm.toRequestBase(),
ID: hm.ID.ValueString(),
}
}
// toRequestBase builds the common request data structure for the PCI hardware mapping creation or update API calls.
func (hm *modelPCI) toRequestBase() apitypes.DataBase {
dataBase := apitypes.DataBase{
// The attribute is named "description" by the Proxmox VE API, but we map it as a comment since this naming is
// generally across the Proxmox VE web UI and API documentations.
Description: hm.Comment.ValueStringPointer(),
}
maps := make([]proxmoxtypes.Map, len(hm.Map))
for idx, tfMap := range hm.Map {
pveMap := proxmoxtypes.Map{
// The attribute is named "description" by the Proxmox VE API, but we map it as a comment since this naming is
// generally across the Proxmox VE web UI and API documentations.
Description: tfMap.Comment.ValueStringPointer(),
ID: proxmoxtypes.DeviceID(tfMap.ID.ValueString()),
IOMMUGroup: tfMap.IOMMUGroup.ValueInt64Pointer(),
Node: tfMap.Node.ValueString(),
Path: tfMap.Path.ValueStringPointer(),
SubsystemID: proxmoxtypes.DeviceID(tfMap.SubsystemID.ValueString()),
}
maps[idx] = pveMap
}
dataBase.Map = maps
dataBase.MediatedDevices.FromValue(hm.MediatedDevices)
return dataBase
}
// toUpdateRequest builds the request data structure for updating an existing PCI hardware mapping.
func (hm *modelPCI) toUpdateRequest(currentState *modelPCI) *apitypes.UpdateRequestBody {
var del []string
baseRequest := hm.toRequestBase()
if hm.Comment.IsNull() && !currentState.Comment.IsNull() {
// The Proxmox VE API attribute is named "description" while we name it "comment" internally since this naming is
// generally used across the Proxmox VE web UI and API documentations.
// This still follows theTerraform "best practices" [1] as it improves the user experience by matching the field
// name to the naming used in the human-facing interfaces.
// References:
// 1. https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles#resource-and-attribute-schema-should-closely-match-the-underlying-api
del = append(del, proxmoxtypes.AttrNameDescription)
}
if hm.MediatedDevices.IsNull() || !hm.MediatedDevices.ValueBool() {
del = append(del, apitypes.APIParamNamePCIMediatedDevices)
baseRequest.MediatedDevices.FromValue(types.BoolValue(false))
}
return &apitypes.UpdateRequestBody{
DataBase: baseRequest,
Delete: del,
}
}
// importFromAPI imports the contents of a USB hardware mapping model from the Proxmox VE API's response data.
func (hm *modelUSB) importFromAPI(_ context.Context, data *apitypes.GetResponseData) {
// Ensure that both the ID and name are in sync.
hm.Name = hm.ID
// The attribute is named "description" by the Proxmox VE API, but we map it as a comment since this naming is
// generally across the Proxmox VE web UI and API documentations.
hm.Comment = types.StringPointerValue(data.Description)
maps := make([]modelUSBMap, len(data.Map))
for idx, pveMap := range data.Map {
tfMap := modelUSBMap{
ID: pveMap.ID.ToValue(),
Node: types.StringValue(pveMap.Node),
Path: customtypes.NewPathPointerValue(pveMap.Path),
}
if pveMap.Description != nil {
tfMap.Comment = types.StringPointerValue(pveMap.Description)
}
maps[idx] = tfMap
}
hm.Map = maps
}
// toCreateRequest builds the request data structure for creating a new USB hardware mapping.
func (hm *modelUSB) toCreateRequest() *apitypes.CreateRequestBody {
return &apitypes.CreateRequestBody{
DataBase: hm.toRequestBase(),
ID: hm.ID.ValueString(),
}
}
// toRequestBase builds the common request data structure for the USB hardware mapping creation or update API calls.
func (hm *modelUSB) toRequestBase() apitypes.DataBase {
dataBase := apitypes.DataBase{
// The attribute is named "description" by the Proxmox VE API, but we map it as a comment since this naming is
// generally across the Proxmox VE web UI and API documentations.
Description: hm.Comment.ValueStringPointer(),
}
maps := make([]proxmoxtypes.Map, len(hm.Map))
for idx, tfMap := range hm.Map {
pveMap := proxmoxtypes.Map{
ID: proxmoxtypes.DeviceID(tfMap.ID.ValueString()),
Node: tfMap.Node.ValueString(),
Path: tfMap.Path.ValueStringPointer(),
}
if !tfMap.Comment.IsNull() {
// The attribute is named "description" by the Proxmox VE API, but we map it as a comment since this naming is
// generally across the Proxmox VE web UI and API documentations.
pveMap.Description = tfMap.Comment.ValueStringPointer()
}
maps[idx] = pveMap
}
dataBase.Map = maps
return dataBase
}
// toUpdateRequest builds the request data structure for updating an existing USB hardware mapping.
func (hm *modelUSB) toUpdateRequest(currentState *modelUSB) *apitypes.UpdateRequestBody {
var del []string
if hm.Comment.IsNull() && !currentState.Comment.IsNull() {
// The Proxmox VE API attribute is named "description" while we name it "comment" internally since this naming is
// generally used across the Proxmox VE web UI and API documentations.
// This still follows the Terraform "best practices" [1] as it improves the user experience by matching the field
// name to the naming used in the human-facing interfaces.
// References:
// 1. https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles#resource-and-attribute-schema-should-closely-match-the-underlying-api
del = append(del, proxmoxtypes.AttrNameDescription)
}
return &apitypes.UpdateRequestBody{
DataBase: hm.toRequestBase(),
Delete: del,
}
}

View File

@ -0,0 +1,308 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package hardwaremapping
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/bpg/terraform-provider-proxmox/fwprovider/structure"
customtypes "github.com/bpg/terraform-provider-proxmox/fwprovider/types/hardwaremapping"
"github.com/bpg/terraform-provider-proxmox/fwprovider/validators"
"github.com/bpg/terraform-provider-proxmox/proxmox"
mappings "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/mapping"
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/hardwaremapping"
)
// Ensure the resource implements the required interfaces.
var (
_ resource.Resource = &resourcePCI{}
_ resource.ResourceWithConfigure = &resourcePCI{}
_ resource.ResourceWithImportState = &resourcePCI{}
)
// resourcePCI contains the PCI hardware mapping resource's internal data.
type resourcePCI struct {
// client is the hardware mapping API client.
client mappings.Client
}
// read reads information about a PCI hardware mapping from the Proxmox VE API.
func (r *resourcePCI) read(ctx context.Context, hm *modelPCI) (bool, diag.Diagnostics) {
var diags diag.Diagnostics
hmName := hm.Name.ValueString()
data, err := r.client.Get(ctx, proxmoxtypes.TypePCI, hmName)
if err != nil {
if strings.Contains(err.Error(), "no such resource") {
diags.AddError("Could not read PCI hardware mapping", err.Error())
}
return false, diags
}
hm.importFromAPI(ctx, data)
return true, nil
}
// readBack reads information about a created or modified PCI hardware mapping from the Proxmox VE API then updates the
// response state accordingly.
// The Terraform resource identifier must have been set in the state before this method is called!
func (r *resourcePCI) readBack(ctx context.Context, hm *modelPCI, respDiags *diag.Diagnostics, respState *tfsdk.State) {
found, diags := r.read(ctx, hm)
respDiags.Append(diags...)
if !found {
respDiags.AddError(
"PCI hardware mapping resource not found after update",
"Failed to find the resource when trying to read back the updated PCI hardware mapping's data.",
)
}
if !respDiags.HasError() {
respDiags.Append(respState.Set(ctx, *hm)...)
}
}
// Configure adds the provider-configured client to the resource.
func (r *resourcePCI) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(proxmox.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *proxmox.Client, got: %T", req.ProviderData),
)
}
r.client = *client.Cluster().HardwareMapping()
}
// Create creates a new PCI hardware mapping.
func (r *resourcePCI) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var hm modelPCI
resp.Diagnostics.Append(req.Plan.Get(ctx, &hm)...)
if resp.Diagnostics.HasError() {
return
}
hmName := hm.Name.ValueString()
// Ensure to keep both in sync since the name represents the ID.
hm.ID = hm.Name
if err := r.client.Create(ctx, proxmoxtypes.TypePCI, hm.toCreateRequest()); err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Could not create PCI hardware mapping %q.", hmName),
err.Error(),
)
return
}
r.readBack(ctx, &hm, &resp.Diagnostics, &resp.State)
}
// Delete deletes an existing PCI hardware mapping.
func (r *resourcePCI) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var hm modelPCI
resp.Diagnostics.Append(req.State.Get(ctx, &hm)...)
if resp.Diagnostics.HasError() {
return
}
hmID := hm.Name.ValueString()
if err := r.client.Delete(ctx, proxmoxtypes.TypePCI, hmID); err != nil {
if strings.Contains(err.Error(), "no such resource") {
resp.Diagnostics.AddWarning(
"PCI hardware mapping does not exist",
fmt.Sprintf(
"Could not delete PCI hardware mapping %q, it does not exist or has been deleted outside of Terraform.",
hmID,
),
)
} else {
resp.Diagnostics.AddError(fmt.Sprintf("Could not delete PCI hardware mapping %q.", hmID), err.Error())
}
}
}
// ImportState imports a PCI hardware mapping from the Proxmox VE API.
func (r *resourcePCI) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
data := modelPCI{
ID: types.StringValue(req.ID),
Name: types.StringValue(req.ID),
}
resource.ImportStatePassthroughID(ctx, path.Root(schemaAttrNameTerraformID), req, resp)
r.readBack(ctx, &data, &resp.Diagnostics, &resp.State)
}
// Metadata defines the name of the PCI hardware mapping.
func (r *resourcePCI) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_hardware_mapping_pci"
}
// Read reads the PCI hardware mapping.
func (r *resourcePCI) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data modelPCI
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
found, diags := r.read(ctx, &data)
resp.Diagnostics.Append(diags...)
if !resp.Diagnostics.HasError() {
if found {
resp.Diagnostics.Append(resp.State.Set(ctx, data)...)
} else {
resp.State.RemoveResource(ctx)
}
}
}
// Schema defines the schema for the PCI hardware mapping.
func (r *resourcePCI) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
comment := resourceSchemaBaseAttrComment
comment.Description = "The comment of this PCI hardware mapping."
commentMap := comment
commentMap.Description = "The comment of the mapped PCI device."
resp.Schema = schema.Schema{
Description: "Manages a PCI hardware mapping in a Proxmox VE cluster.",
Attributes: map[string]schema.Attribute{
schemaAttrNameComment: comment,
schemaAttrNameMap: schema.SetNestedAttribute{
Description: "The actual map of devices for the PCI hardware mapping.",
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
schemaAttrNameComment: commentMap,
schemaAttrNameMapDeviceID: schema.StringAttribute{
Description: "The ID of the map.",
Required: true,
Validators: []validator.String{
validators.HardwareMappingDeviceIDValidator(),
},
},
schemaAttrNameMapIOMMUGroup: schema.Int64Attribute{
Description: "The IOMMU group of the map. Not mandatory for the Proxmox VE API call, " +
"but causes a PCI hardware mapping to be incomplete when not set",
Optional: true,
},
schemaAttrNameMapNode: schema.StringAttribute{
Description: "The node name of the map.",
Required: true,
},
schemaAttrNameMapPath: schema.StringAttribute{
CustomType: customtypes.PathType{},
Description: "The path of the map.",
// For hardware mappings of type PCI, the path is required while it is optional for USB.
Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
customtypes.PathPCIValueRegEx,
ErrResourceMessageInvalidPath(proxmoxtypes.TypePCI),
),
},
},
schemaAttrNameMapSubsystemID: schema.StringAttribute{
Description: "The subsystem ID group of the map. Not mandatory for the Proxmox VE API call, " +
"but causes a PCI hardware mapping to be incomplete when not set",
Optional: true,
Validators: []validator.String{
validators.HardwareMappingDeviceIDValidator(),
},
},
},
},
Required: true,
Validators: []validator.Set{
setvalidator.SizeAtLeast(1),
},
},
schemaAttrNameMediatedDevices: schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
Description: "Indicates whether to enable mediated devices.",
},
schemaAttrNameName: schema.StringAttribute{
Description: "The name of this PCI hardware mapping.",
Required: true,
},
schemaAttrNameTerraformID: structure.IDAttribute(
"The unique identifier of this PCI hardware mapping resource.",
),
},
}
}
// Update updates an existing PCI hardware mapping.
func (r *resourcePCI) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var hmCurrent, hmPlan modelPCI
resp.Diagnostics.Append(req.Plan.Get(ctx, &hmPlan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &hmCurrent)...)
if resp.Diagnostics.HasError() {
return
}
hmName := hmPlan.Name.ValueString()
if err := r.client.Update(
ctx,
proxmoxtypes.TypePCI,
hmName,
hmPlan.toUpdateRequest(&hmCurrent),
); err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Could not update PCI hardware mapping %q.", hmName),
err.Error(),
)
return
}
r.readBack(ctx, &hmPlan, &resp.Diagnostics, &resp.State)
}
// NewResourcePCI returns a new resource for managing a PCI hardware mapping.
// This is a helper function to simplify the provider implementation.
func NewResourcePCI() resource.Resource {
return &resourcePCI{}
}

View File

@ -0,0 +1,290 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package hardwaremapping
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/bpg/terraform-provider-proxmox/fwprovider/structure"
customtypes "github.com/bpg/terraform-provider-proxmox/fwprovider/types/hardwaremapping"
"github.com/bpg/terraform-provider-proxmox/fwprovider/validators"
"github.com/bpg/terraform-provider-proxmox/proxmox"
mappings "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/mapping"
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/hardwaremapping"
)
// Ensure the resource implements the required interfaces.
var (
_ resource.Resource = &resourceUSB{}
_ resource.ResourceWithConfigure = &resourceUSB{}
_ resource.ResourceWithImportState = &resourceUSB{}
)
// resourceUSB contains the USB hardware mapping resource's internal data.
type resourceUSB struct {
// client is the hardware mapping API client.
client mappings.Client
}
// read reads information about a USB hardware mapping from the Proxmox VE API.
func (r *resourceUSB) read(ctx context.Context, hm *modelUSB) (bool, diag.Diagnostics) {
var diags diag.Diagnostics
hmName := hm.Name.ValueString()
data, err := r.client.Get(ctx, proxmoxtypes.TypeUSB, hmName)
if err != nil {
if strings.Contains(err.Error(), "no such resource") {
diags.AddError("Could not read USB hardware mapping", err.Error())
}
return false, diags
}
hm.importFromAPI(ctx, data)
return true, nil
}
// readBack reads information about a created or modified USB hardware mapping from the Proxmox VE API then updates the
// response state accordingly.
// The Terraform resource identifier must have been set in the state before this method is called!
func (r *resourceUSB) readBack(ctx context.Context, hm *modelUSB, respDiags *diag.Diagnostics, respState *tfsdk.State) {
found, diags := r.read(ctx, hm)
respDiags.Append(diags...)
if !found {
respDiags.AddError(
"USB hardware mapping resource not found after update",
"Failed to find the resource when trying to read back the updated USB hardware mapping's data.",
)
}
if !respDiags.HasError() {
respDiags.Append(respState.Set(ctx, *hm)...)
}
}
// Configure adds the provider-configured client to the resource.
func (r *resourceUSB) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(proxmox.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *proxmox.Client, got: %T", req.ProviderData),
)
}
r.client = *client.Cluster().HardwareMapping()
}
// Create creates a new USB hardware mapping.
func (r *resourceUSB) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var hm modelUSB
resp.Diagnostics.Append(req.Plan.Get(ctx, &hm)...)
if resp.Diagnostics.HasError() {
return
}
hmName := hm.Name.ValueString()
// Ensure to keep both in sync since the name represents the ID.
hm.ID = hm.Name
apiReq := hm.toCreateRequest()
if err := r.client.Create(ctx, proxmoxtypes.TypeUSB, apiReq); err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Could not create USB hardware mapping %q.", hmName),
err.Error(),
)
return
}
r.readBack(ctx, &hm, &resp.Diagnostics, &resp.State)
}
// Delete deletes an existing USB hardware mapping.
func (r *resourceUSB) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var hm modelUSB
resp.Diagnostics.Append(req.State.Get(ctx, &hm)...)
if resp.Diagnostics.HasError() {
return
}
hmID := hm.Name.ValueString()
if err := r.client.Delete(ctx, proxmoxtypes.TypeUSB, hmID); err != nil {
if strings.Contains(err.Error(), "no such resource") {
resp.Diagnostics.AddWarning(
"USB hardware mapping does not exist",
fmt.Sprintf(
"Could not delete USB hardware mapping %q, it does not exist or has been deleted outside of Terraform.",
hmID,
),
)
} else {
resp.Diagnostics.AddError(fmt.Sprintf("Could not delete USB hardware mapping %q.", hmID), err.Error())
}
}
}
// ImportState imports a USB hardware mapping from the Proxmox VE API.
func (r *resourceUSB) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
data := modelUSB{
ID: types.StringValue(req.ID),
Name: types.StringValue(req.ID),
}
resource.ImportStatePassthroughID(ctx, path.Root(schemaAttrNameTerraformID), req, resp)
r.readBack(ctx, &data, &resp.Diagnostics, &resp.State)
}
// Metadata defines the name of the USB hardware mapping.
func (r *resourceUSB) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_hardware_mapping_usb"
}
// Read reads the USB hardware mapping.
//
func (r *resourceUSB) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data modelUSB
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
found, diags := r.read(ctx, &data)
resp.Diagnostics.Append(diags...)
if !resp.Diagnostics.HasError() {
if found {
resp.Diagnostics.Append(resp.State.Set(ctx, data)...)
} else {
resp.State.RemoveResource(ctx)
}
}
}
// Schema defines the schema for the USB hardware mapping.
func (r *resourceUSB) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
comment := resourceSchemaBaseAttrComment
comment.Description = "The comment of this USB hardware mapping."
commentMap := comment
commentMap.Description = "The comment of the mapped USB device."
resp.Schema = schema.Schema{
Description: "Manages a USB hardware mapping in a Proxmox VE cluster.",
Attributes: map[string]schema.Attribute{
schemaAttrNameComment: comment,
schemaAttrNameMap: schema.SetNestedAttribute{
Description: "The actual map of devices for the hardware mapping.",
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
schemaAttrNameComment: commentMap,
schemaAttrNameMapDeviceID: schema.StringAttribute{
Description: "The ID of the map.",
Required: true,
Validators: []validator.String{
validators.HardwareMappingDeviceIDValidator(),
},
},
schemaAttrNameMapNode: schema.StringAttribute{
Description: "The node name of the map.",
Required: true,
},
schemaAttrNameMapPath: schema.StringAttribute{
CustomType: customtypes.PathType{},
Description: "The path of the map. For hardware mappings of type USB the path is optional and indicates" +
" that the device is mapped through the device ID instead of ports.",
Optional: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
customtypes.PathUSBValueRegEx,
ErrResourceMessageInvalidPath(proxmoxtypes.TypeUSB),
),
},
},
},
},
Required: true,
Validators: []validator.Set{
setvalidator.SizeAtLeast(1),
},
},
schemaAttrNameName: schema.StringAttribute{
Description: "The name of this hardware mapping.",
Required: true,
},
schemaAttrNameTerraformID: structure.IDAttribute(
"The unique identifier of this USB hardware mapping resource.",
),
},
}
}
// Update updates an existing USB hardware mapping.
func (r *resourceUSB) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var hmCurrent, hmPlan modelUSB
resp.Diagnostics.Append(req.Plan.Get(ctx, &hmPlan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &hmCurrent)...)
if resp.Diagnostics.HasError() {
return
}
hmName := hmPlan.Name.ValueString()
apiReq := hmPlan.toUpdateRequest(&hmCurrent)
if err := r.client.Update(ctx, proxmoxtypes.TypeUSB, hmName, apiReq); err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Could not update USB hardware mapping %q.", hmName),
err.Error(),
)
return
}
r.readBack(ctx, &hmPlan, &resp.Diagnostics, &resp.State)
}
// NewResourceUSB returns a new resource for managing a USB hardware mapping.
// This is a helper function to simplify the provider implementation.
func NewResourceUSB() resource.Resource {
return &resourceUSB{}
}

View File

@ -0,0 +1,43 @@
package hardwaremapping
import (
"regexp"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
resourceschema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
)
//nolint:gochecknoglobals
var (
// dataSourceSchemaBaseAttrComment is the base comment attribute for a hardware mapping data source.
// Note that the Proxmox VE API attribute is named "description", but we map it as a comment since this naming is
// generally across the Proxmox VE web UI and API documentations. This still follows the [Terraform "best practices"]
// as it improves the user experience by matching the field name to the naming used in the human-facing interfaces.
//
// [Terraform "best practices"]: https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles#resource-and-attribute-schema-should-closely-match-the-underlying-api
dataSourceSchemaBaseAttrComment = datasourceschema.StringAttribute{
Optional: true,
Validators: []validator.String{
stringvalidator.UTF8LengthAtLeast(1),
stringvalidator.RegexMatches(regexp.MustCompile(`^\S|^$`), "must not start with whitespace"),
stringvalidator.RegexMatches(regexp.MustCompile(`\S$|^$`), "must not end with whitespace"),
},
}
// dataSourceSchemaBaseAttrComment is the base comment attribute for a hardware mapping resource.
// Note that the Proxmox VE API attribute is named "description", but we map it as a comment since this naming is
// generally across the Proxmox VE web UI and API documentations. This still follows the [Terraform "best practices"]
// as it improves the user experience by matching the field name to the naming used in the human-facing interfaces.
//
// [Terraform "best practices"]: https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles#resource-and-attribute-schema-should-closely-match-the-underlying-api
resourceSchemaBaseAttrComment = resourceschema.StringAttribute{
Optional: true,
Validators: []validator.String{
stringvalidator.UTF8LengthAtLeast(1),
stringvalidator.RegexMatches(regexp.MustCompile(`^\S|^$`), "must not start with whitespace"),
stringvalidator.RegexMatches(regexp.MustCompile(`\S$|^$`), "must not end with whitespace"),
},
}
)

View File

@ -26,6 +26,7 @@ import (
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/bpg/terraform-provider-proxmox/fwprovider/ha"
"github.com/bpg/terraform-provider-proxmox/fwprovider/hardwaremapping"
"github.com/bpg/terraform-provider-proxmox/fwprovider/network"
"github.com/bpg/terraform-provider-proxmox/proxmox"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
@ -440,6 +441,8 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc
return []func() resource.Resource{
ha.NewHAGroupResource,
ha.NewHAResourceResource,
hardwaremapping.NewResourcePCI,
hardwaremapping.NewResourceUSB,
network.NewLinuxBridgeResource,
network.NewLinuxVLANResource,
NewClusterOptionsResource,
@ -453,6 +456,9 @@ func (p *proxmoxProvider) DataSources(_ context.Context) []func() datasource.Dat
ha.NewHAGroupDataSource,
ha.NewHAResourcesDataSource,
ha.NewHAResourceDataSource,
hardwaremapping.NewDataSourcePCI,
hardwaremapping.NewDataSourceUSB,
hardwaremapping.NewDataSource,
NewVersionDataSource,
}
}

View File

@ -0,0 +1,706 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package tests
import (
"fmt"
"regexp"
"strconv"
"testing"
"github.com/brianvoe/gofakeit/v7"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
hwm "github.com/bpg/terraform-provider-proxmox/fwprovider/hardwaremapping"
customtypes "github.com/bpg/terraform-provider-proxmox/fwprovider/types/hardwaremapping"
"github.com/bpg/terraform-provider-proxmox/fwprovider/validators"
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/hardwaremapping"
)
const (
accTestHardwareMappingNamePCI = "proxmox_virtual_environment_hardware_mapping_pci.test"
accTestHardwareMappingNameUSB = "proxmox_virtual_environment_hardware_mapping_usb.test"
)
type accTestHardwareMappingFakeData struct {
Comments []string `fake:"{sentence:3}" fakesize:"2"`
MapComments []string `fake:"{sentence:3}" fakesize:"2"`
MapDeviceIDs []string `fake:"{linuxdeviceid}" fakesize:"2"`
MapIOMMUGroups []uint `fake:"{number:1,20}" fakesize:"2"`
MapPathsPCI []string `fake:"{linuxdevicepathpci}" fakesize:"2"`
MapPathsUSB []string `fake:"{linuxdevicepathusb}" fakesize:"2"`
MapSubsystemIDs []string `fake:"{linuxdeviceid}" fakesize:"2"`
MediatedDevices bool `fake:"{bool}"`
Names []string `fake:"{noun}" fakesize:"2"`
}
func testAccResourceHardwareMappingInit(t *testing.T) (*accTestHardwareMappingFakeData, *testEnvironment) {
t.Helper()
// Register a new custom function to generate random Linux device IDs.
gofakeit.AddFuncLookup(
"linuxdeviceid", gofakeit.Info{
Category: "custom",
Description: "Random Linux device ID",
Example: "8086:5916",
Output: "string",
Generate: func(f *gofakeit.Faker, _ *gofakeit.MapParams, _ *gofakeit.Info) (any, error) {
return f.Regex(proxmoxtypes.DeviceIDAttrValueRegEx.String()), nil
},
},
)
// Register a new custom function to generate random Linux PCI device paths.
gofakeit.AddFuncLookup(
"linuxdevicepathpci", gofakeit.Info{
Category: "custom",
Description: "Random Linux PCI device path",
Example: "0000:00:02.0",
Output: "string",
Generate: func(f *gofakeit.Faker, _ *gofakeit.MapParams, _ *gofakeit.Info) (any, error) {
return f.Regex(customtypes.PathPCIValueRegEx.String()), nil
},
},
)
// Register a new custom function to generate random Linux USB device paths.
gofakeit.AddFuncLookup(
"linuxdevicepathusb", gofakeit.Info{
Category: "custom",
Description: "Random Linux USB device path",
Example: "1-5.2",
Output: "string",
Generate: func(f *gofakeit.Faker, _ *gofakeit.MapParams, _ *gofakeit.Info) (any, error) {
return f.Regex(customtypes.PathUSBValueRegEx.String()), nil
},
},
)
te := initTestEnvironment(t)
var data accTestHardwareMappingFakeData
if err := gofakeit.Struct(&data); err != nil {
t.Fatalf("could not create fake data for hardware mapping: %s", err)
}
return &data, te
}
// TestAccResourceHardwareMappingPCIValidInput runs tests for PCI hardware mapping resource definitions with valid input
// where all possible attributes are
// specified.
// All implementations of the [github.com/hashicorp/terraform-plugin-framework/resource.Resource] interface are tested
// in sequential steps.
func TestAccResourceHardwareMappingPCIValidInput(t *testing.T) {
data, te := testAccResourceHardwareMappingInit(t)
resource.Test(
t, resource.TestCase{
ProtoV6ProviderFactories: te.accProviders,
Steps: []resource.TestStep{
// Test the "Create" and "Read" implementations where all possible attributes are specified.
{
Config: fmt.Sprintf(
`
resource "proxmox_virtual_environment_hardware_mapping_pci" "test" {
comment = "%s"
name = "%s"
map = [
{
comment = "%s"
id = "%s"
iommu_group = %d
node = "%s"
path = "%s"
subsystem_id = "%s"
},
]
mediated_devices = %t
}
`,
data.Comments[0],
data.Names[0],
data.MapComments[0],
data.MapDeviceIDs[0],
data.MapIOMMUGroups[0],
te.nodeName,
data.MapPathsPCI[0],
data.MapSubsystemIDs[0],
data.MediatedDevices,
),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(accTestHardwareMappingNamePCI, "comment", data.Comments[0]),
resource.TestCheckResourceAttrSet(accTestHardwareMappingNamePCI, "id"),
resource.TestCheckTypeSetElemNestedAttrs(
accTestHardwareMappingNamePCI, "map.*", map[string]string{
"comment": data.MapComments[0],
"id": data.MapDeviceIDs[0],
"iommu_group": strconv.Itoa(int(data.MapIOMMUGroups[0])),
"node": te.nodeName,
"path": data.MapPathsPCI[0],
"subsystem_id": data.MapSubsystemIDs[0],
},
),
resource.TestCheckResourceAttr(
accTestHardwareMappingNamePCI,
"mediated_devices",
strconv.FormatBool(data.MediatedDevices),
),
resource.TestCheckResourceAttr(accTestHardwareMappingNamePCI, "name", data.Names[0]),
),
},
// Test the "ImportState" implementation.
{
ImportState: true,
ImportStateId: data.Names[0],
ImportStateVerify: true,
ResourceName: accTestHardwareMappingNamePCI,
},
// Test the "Update" implementation where all possible attributes are specified.
{
Config: fmt.Sprintf(
`
resource "proxmox_virtual_environment_hardware_mapping_pci" "test" {
comment = "%s"
name = "%s"
map = [
{
comment = "%s"
id = "%s"
iommu_group = %d
node = "%s"
path = "%s"
subsystem_id = "%s"
},
]
mediated_devices = %t
}
`,
data.Comments[1],
data.Names[0],
data.MapComments[1],
data.MapDeviceIDs[0],
data.MapIOMMUGroups[1],
te.nodeName,
data.MapPathsPCI[1],
data.MapSubsystemIDs[1],
!data.MediatedDevices,
),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(accTestHardwareMappingNamePCI, "comment", data.Comments[1]),
resource.TestCheckResourceAttrSet(accTestHardwareMappingNamePCI, "id"),
resource.TestCheckTypeSetElemNestedAttrs(
accTestHardwareMappingNamePCI, "map.*", map[string]string{
"comment": data.MapComments[1],
"id": data.MapDeviceIDs[0],
"iommu_group": strconv.Itoa(int(data.MapIOMMUGroups[1])),
"node": te.nodeName,
"path": data.MapPathsPCI[1],
"subsystem_id": data.MapSubsystemIDs[1],
},
),
resource.TestCheckResourceAttr(
accTestHardwareMappingNamePCI,
"mediated_devices",
strconv.FormatBool(!data.MediatedDevices),
),
resource.TestCheckResourceAttr(accTestHardwareMappingNamePCI, "name", data.Names[0]),
),
},
},
},
)
}
// TestAccResourceHardwareMappingPCIValidInputMinimal runs tests for PCI hardware mapping resource definitions with
// valid input that only have the minimum
// amount of attributes set to test computed and default values within the resulting plan and state. The last step sets
// the undefined values to test the update
// logic.
// All implementations of the [github.com/hashicorp/terraform-plugin-framework/resource.Resource] interface are tested
// in sequential steps.
func TestAccResourceHardwareMappingPCIValidInputMinimal(t *testing.T) {
data, te := testAccResourceHardwareMappingInit(t)
resource.Test(
t, resource.TestCase{
ProtoV6ProviderFactories: te.accProviders,
Steps: []resource.TestStep{
// Test the "Create" and "Read" implementations with only the minimum amount of attributes being set.
{
Config: fmt.Sprintf(
`
resource "proxmox_virtual_environment_hardware_mapping_pci" "test" {
name = "%s"
map = [
{
id = "%s"
node = "%s"
path = "%s"
},
]
}
`,
data.Names[0],
data.MapDeviceIDs[0],
te.nodeName,
data.MapPathsPCI[0],
),
ConfigStateChecks: []statecheck.StateCheck{
// Optional attributes should all be unset.
statecheck.ExpectKnownValue(
accTestHardwareMappingNamePCI,
tfjsonpath.New("map").AtSliceIndex(0),
knownvalue.MapPartial(
map[string]knownvalue.Check{
"comment": knownvalue.Null(),
"iommu_group": knownvalue.Null(),
"subsystem_id": knownvalue.Null(),
},
),
),
},
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(accTestHardwareMappingNamePCI, "id"),
resource.TestCheckTypeSetElemNestedAttrs(
accTestHardwareMappingNamePCI, "map.*", map[string]string{
"id": data.MapDeviceIDs[0],
"node": te.nodeName,
"path": data.MapPathsPCI[0],
},
),
resource.TestCheckResourceAttr(accTestHardwareMappingNamePCI, "name", data.Names[0]),
),
},
// Test the "ImportState" implementation.
{
ImportState: true,
ImportStateId: data.Names[0],
ImportStateVerify: true,
ResourceName: accTestHardwareMappingNamePCI,
},
// Test the "Update" implementation by setting all previously undefined attributes.
{
Config: fmt.Sprintf(
`
resource "proxmox_virtual_environment_hardware_mapping_pci" "test" {
comment = "%s"
name = "%s"
map = [
{
comment = "%s"
id = "%s"
iommu_group = %d
node = "%s"
path = "%s"
subsystem_id = "%s"
},
]
mediated_devices = %t
}
`,
data.Comments[1],
data.Names[0],
data.MapComments[1],
data.MapDeviceIDs[0],
data.MapIOMMUGroups[1],
te.nodeName,
data.MapPathsPCI[1],
data.MapSubsystemIDs[1],
!data.MediatedDevices,
),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(accTestHardwareMappingNamePCI, "comment", data.Comments[1]),
resource.TestCheckResourceAttrSet(accTestHardwareMappingNamePCI, "id"),
resource.TestCheckTypeSetElemNestedAttrs(
accTestHardwareMappingNamePCI, "map.*", map[string]string{
"comment": data.MapComments[1],
"id": data.MapDeviceIDs[0],
"iommu_group": strconv.Itoa(int(data.MapIOMMUGroups[1])),
"node": te.nodeName,
"path": data.MapPathsPCI[1],
"subsystem_id": data.MapSubsystemIDs[1],
},
),
resource.TestCheckResourceAttr(
accTestHardwareMappingNamePCI,
"mediated_devices",
strconv.FormatBool(!data.MediatedDevices),
),
resource.TestCheckResourceAttr(accTestHardwareMappingNamePCI, "name", data.Names[0]),
),
},
},
},
)
}
// TestAccResourceHardwareMappingPCIInvalidInput runs tests for PCI hardware mapping resource definitions with invalid
// input where all possible attributes are
// specified.
// Only the "Create" method implementation of the [github.com/hashicorp/terraform-plugin-framework/resource.Resource]
// interface is tested in sequential steps.
func TestAccResourceHardwareMappingPCIInvalidInput(t *testing.T) {
data, te := testAccResourceHardwareMappingInit(t)
resource.Test(
t, resource.TestCase{
ProtoV6ProviderFactories: te.accProviders,
Steps: []resource.TestStep{
// Test the "Create" method implementation where all possible attributes are specified, but an error is expected
// when using an invalid device path.
{
ExpectError: regexp.MustCompile(
fmt.Sprintf(
// The error line is, for whatever reason, broken down into multiple lines in acceptance tests, so we need
// to capture newline characters.
// Note that the regular expression syntax used by Go does not capture newlines with the "." matcher,
// so we need to enable the "s" flag that enabled "."
// to match "\n".
// References:
// 1. https://pkg.go.dev/regexp/syntax
`(?s).*%s(?s).*`,
hwm.ErrResourceMessageInvalidPath(proxmoxtypes.TypePCI),
),
),
Config: fmt.Sprintf(
`
resource "proxmox_virtual_environment_hardware_mapping_pci" "test" {
comment = "%s"
name = "%s"
map = [
{
comment = "%s"
id = "%s"
iommu_group = %d
node = "%s"
# Only valid Linux PCI device paths should pass the verification.
path = "wxyz:1337"
subsystem_id = "%s"
},
]
mediated_devices = %t
}
`,
data.Comments[0],
data.Names[0],
data.Comments[1],
data.MapDeviceIDs[0],
data.MapIOMMUGroups[0],
te.nodeName,
data.MapSubsystemIDs[0],
data.MediatedDevices,
),
},
},
},
)
resource.Test(
t, resource.TestCase{
ProtoV6ProviderFactories: te.accProviders,
Steps: []resource.TestStep{
// Test the "Create" method implementation where all possible attributes are specified, but an error is expected
// when using an invalid device subsystem
// ID.
{
ExpectError: regexp.MustCompile(fmt.Sprintf(`.*%s.*`, validators.HardwareMappingDeviceIDValidatorErrMessage)),
Config: fmt.Sprintf(
`
resource "proxmox_virtual_environment_hardware_mapping_pci" "test" {
comment = "%s"
name = "%s"
map = [
{
comment = "%s"
id = "%s"
iommu_group = %d
node = "%s"
path = "%s"
# Only valid Linux device subsystem IDs should pass the verification.
subsystem_id = "x1y2:1337"
},
]
mediated_devices = %t
}
`,
data.Comments[0],
data.Names[0],
data.Comments[1],
data.MapDeviceIDs[0],
data.MapIOMMUGroups[0],
te.nodeName,
data.MapPathsPCI[0],
data.MediatedDevices,
),
},
},
},
)
}
// TestAccResourceHardwareMappingUSBValidInput runs tests for USB hardware mapping resource definitions with valid input
// where all possible attributes are
// specified.
// All implementations of the [github.com/hashicorp/terraform-plugin-framework/resource.Resource] interface are tested
// in sequential steps.
func TestAccResourceHardwareMappingUSBValidInput(t *testing.T) {
data, te := testAccResourceHardwareMappingInit(t)
resource.Test(
t, resource.TestCase{
ProtoV6ProviderFactories: te.accProviders,
Steps: []resource.TestStep{
// Test the "Create" and "Read" implementations where all possible attributes are specified.
{
Config: fmt.Sprintf(
`
resource "proxmox_virtual_environment_hardware_mapping_usb" "test" {
comment = "%s"
name = "%s"
map = [
{
comment = "%s"
id = "%s"
node = "%s"
path = "%s"
},
]
}
`,
data.Comments[0],
data.Names[0],
data.MapComments[0],
data.MapDeviceIDs[0],
te.nodeName,
data.MapPathsUSB[0],
),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(accTestHardwareMappingNameUSB, "comment", data.Comments[0]),
resource.TestCheckResourceAttrSet(accTestHardwareMappingNameUSB, "id"),
resource.TestCheckTypeSetElemNestedAttrs(
accTestHardwareMappingNameUSB, "map.*", map[string]string{
"comment": data.MapComments[0],
"id": data.MapDeviceIDs[0],
"node": te.nodeName,
"path": data.MapPathsUSB[0],
},
),
resource.TestCheckResourceAttr(accTestHardwareMappingNameUSB, "name", data.Names[0]),
),
},
// Test the "ImportState" implementation and ensure that PCI-only attributes are not set.
{
ImportState: true,
ImportStateId: data.Names[0],
ImportStateVerify: true,
ResourceName: accTestHardwareMappingNameUSB,
},
// Test the "Update" implementation where all possible attributes are specified.
{
Config: fmt.Sprintf(
`
resource "proxmox_virtual_environment_hardware_mapping_usb" "test" {
comment = "%s"
name = "%s"
map = [
{
comment = "%s"
id = "%s"
node = "%s"
path = "%s"
},
]
}
`,
data.Comments[1],
data.Names[0],
data.MapComments[1],
data.MapDeviceIDs[0],
te.nodeName,
data.MapPathsUSB[1],
),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(accTestHardwareMappingNameUSB, "comment", data.Comments[1]),
resource.TestCheckResourceAttrSet(accTestHardwareMappingNameUSB, "id"),
resource.TestCheckTypeSetElemNestedAttrs(
accTestHardwareMappingNameUSB, "map.*", map[string]string{
"comment": data.MapComments[1],
"id": data.MapDeviceIDs[0],
"node": te.nodeName,
"path": data.MapPathsUSB[1],
},
),
resource.TestCheckResourceAttr(accTestHardwareMappingNameUSB, "name", data.Names[0]),
),
},
},
},
)
}
// TestAccResourceHardwareMappingUSBValidInputMinimal runs tests for USB hardware mapping resource definitions with
// valid input that only have the minimum
// amount of attributes set to test computed and default values within the resulting plan and state. The last step sets
// the undefined values to test the update
// logic.
// All implementations of the [github.com/hashicorp/terraform-plugin-framework/resource.Resource] interface are tested
// in sequential steps.
func TestAccResourceHardwareMappingUSBValidInputMinimal(t *testing.T) {
data, te := testAccResourceHardwareMappingInit(t)
resource.Test(
t, resource.TestCase{
ProtoV6ProviderFactories: te.accProviders,
Steps: []resource.TestStep{
// Test the "Create" and "Read" implementations with only the minimum amount of attributes being set.
{
Config: fmt.Sprintf(
`
resource "proxmox_virtual_environment_hardware_mapping_usb" "test" {
name = "%s"
map = [
{
id = "%s"
node = "%s"
},
]
}
`,
data.Names[0],
data.MapDeviceIDs[0],
te.nodeName,
),
ConfigStateChecks: []statecheck.StateCheck{
// Optional attributes should all be unset.
statecheck.ExpectKnownValue(
accTestHardwareMappingNameUSB,
tfjsonpath.New("map").AtSliceIndex(0),
knownvalue.MapPartial(
map[string]knownvalue.Check{
"path": knownvalue.Null(),
},
),
),
},
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(accTestHardwareMappingNameUSB, "id"),
resource.TestCheckTypeSetElemNestedAttrs(
accTestHardwareMappingNameUSB, "map.*", map[string]string{
"id": data.MapDeviceIDs[0],
"node": te.nodeName,
},
),
resource.TestCheckResourceAttr(accTestHardwareMappingNameUSB, "name", data.Names[0]),
),
},
// Test the "Update" implementation by setting all previously undefined attributes.
{
Config: fmt.Sprintf(
`
resource "proxmox_virtual_environment_hardware_mapping_usb" "test" {
comment = "%s"
name = "%s"
map = [
{
comment = "%s"
id = "%s"
node = "%s"
path = "%s"
},
]
}
`,
data.Comments[0],
data.Names[0],
data.Comments[1],
data.MapDeviceIDs[1],
te.nodeName,
data.MapPathsUSB[0],
),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(accTestHardwareMappingNameUSB, "comment", data.Comments[0]),
resource.TestCheckResourceAttrSet(accTestHardwareMappingNameUSB, "id"),
resource.TestCheckTypeSetElemNestedAttrs(
accTestHardwareMappingNameUSB, "map.*", map[string]string{
"comment": data.Comments[1],
"id": data.MapDeviceIDs[1],
"node": te.nodeName,
"path": data.MapPathsUSB[0],
},
),
resource.TestCheckResourceAttr(accTestHardwareMappingNameUSB, "name", data.Names[0]),
),
},
},
},
)
}
// TestAccResourceHardwareMappingUSBInvalidInput runs tests for USB hardware mapping resource definitions where all
// possible attributes are specified.
// Only the "Create" method implementation of the [github.com/hashicorp/terraform-plugin-framework/resource.Resource]
// interface is tested in sequential steps.
func TestAccResourceHardwareMappingUSBInvalidInput(t *testing.T) {
data, te := testAccResourceHardwareMappingInit(t)
resource.Test(
t, resource.TestCase{
ProtoV6ProviderFactories: te.accProviders,
Steps: []resource.TestStep{
// Test the "Create" method implementation where all possible attributes are specified, but an error is expected
// when using an invalid device path.
{
ExpectError: regexp.MustCompile(
fmt.Sprintf(
// The error line is, for whatever reason, broken down into multiple lines in acceptance tests, so we need
// to capture newline characters.
// Note that the regular expression syntax used by Go does not capture newlines with the "." matcher,
// so we need to enable the "s" flag that enabled "."
// to match "\n".
// References:
// 1. https://pkg.go.dev/regexp/syntax
`(?s).*%s(?s).*`,
hwm.ErrResourceMessageInvalidPath(proxmoxtypes.TypeUSB),
),
),
Config: fmt.Sprintf(
`
resource "proxmox_virtual_environment_hardware_mapping_usb" "test" {
comment = "%s"
name = "%s"
map = [
{
comment = "%s"
id = "%s"
node = "%s"
# Only valid Linux USB device paths should pass the verification.
path = "xyz3:1337foobar"
},
]
}
`,
data.Comments[0],
data.Names[0],
data.Comments[1],
data.MapDeviceIDs[0],
te.nodeName,
),
},
},
},
)
}

View File

@ -0,0 +1,149 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package hardwaremapping
import (
"context"
"errors"
"fmt"
"regexp"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/hardwaremapping"
)
// Ensure the implementations satisfy the required interfaces.
var (
_ basetypes.StringTypable = PathType{}
_ basetypes.StringValuable = PathValue{}
)
// ErrValueConversion indicates an error while converting a value for a hardware mapping path.
//
//nolint:gochecknoglobals
var ErrValueConversion = func(format string, attrs ...any) error {
return function.NewFuncError(fmt.Sprintf(format, attrs...))
}
var (
// PathPCIValueRegEx is the regular expression for a PCI hardware mapping path.
PathPCIValueRegEx = regexp.MustCompile(`^[a-f0-9]{4,}:[a-f0-9]{2}:[a-f0-9]{2}(\.[a-f0-9])?$`)
// PathUSBValueRegEx is the regular expression for a USB hardware mapping path.
PathUSBValueRegEx = regexp.MustCompile(`^\d+-(\d+)(\.\d+)?$`)
)
// PathType is a type that represents a path of a hardware mapping.
type PathType struct {
basetypes.StringType
Type proxmoxtypes.Type
}
// PathValue is a type that represents the value of a hardware mapping path.
type PathValue struct {
basetypes.StringValue
}
// Equal returns true if the two types are equal.
func (t PathType) Equal(o attr.Type) bool {
other, ok := o.(PathType)
if !ok {
return false
}
return t.StringType.Equal(other.StringType)
}
// String returns a string representation of the type.
func (t PathType) String() string {
return "PathType"
}
// ValueFromString converts a string value to a StringValuable.
func (t PathType) ValueFromString(_ context.Context, in basetypes.StringValue) (
basetypes.StringValuable,
diag.Diagnostics,
) {
value := PathValue{
StringValue: in,
}
return value, nil
}
// ValueFromTerraform converts a Terraform value to a StringValuable.
func (t PathType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
attrValue, err := t.StringType.ValueFromTerraform(ctx, in)
if err != nil {
return nil, errors.Join(
ErrValueConversion("unexpected error converting Terraform value to StringValue"),
err,
)
}
stringValue, ok := attrValue.(basetypes.StringValue)
if !ok {
return nil, ErrValueConversion("unexpected value type of %T", attrValue)
}
stringValuable, diags := t.ValueFromString(ctx, stringValue)
if diags.HasError() {
return nil, ErrValueConversion(
"unexpected error converting StringValue to StringValuable: %v",
diags,
)
}
return stringValuable, nil
}
// ValueType returns the underlying value type.
func (t PathType) ValueType(_ context.Context) attr.Value {
return PathValue{}
}
// Equal returns true if the two values are equal.
func (v PathValue) Equal(o attr.Value) bool {
other, ok := o.(PathValue)
if !ok {
return false
}
return v.StringValue.Equal(other.StringValue)
}
// IsProxmoxType checks whether the value match the given hardware mapping type.
func (v PathValue) IsProxmoxType(hmType proxmoxtypes.Type) bool {
switch hmType {
case proxmoxtypes.TypePCI:
return PathPCIValueRegEx.MatchString(v.ValueString())
case proxmoxtypes.TypeUSB:
return PathUSBValueRegEx.MatchString(v.ValueString()) || v.ValueString() == ""
default:
return false
}
}
// Type returns the type of the value.
func (v PathValue) Type(_ context.Context) attr.Type {
return PathType{}
}
// NewPathPointerValue returns a new PathValue from a string pointer.
func NewPathPointerValue(value *string) PathValue {
return PathValue{
StringValue: types.StringPointerValue(value),
}
}

View File

@ -0,0 +1,72 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package hardwaremapping
import (
"context"
"testing"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)
func TestPathValueFromTerraform(t *testing.T) {
t.Parallel()
tests := map[string]struct {
val tftypes.Value
expected func(val PathValue) bool
expectError bool
}{
"null value": {
val: tftypes.NewValue(tftypes.String, nil),
expected: func(val PathValue) bool {
return val.IsNull()
},
},
"unknown value": {
val: tftypes.NewValue(tftypes.String, tftypes.UnknownValue),
expected: func(val PathValue) bool {
return val.IsUnknown()
},
},
"valid for PCI type": {
val: tftypes.NewValue(tftypes.String, "8086:5916"),
expected: func(val PathValue) bool {
return val.ValueString() == "8086:5916"
},
},
"valid for USB type": {
val: tftypes.NewValue(tftypes.String, "1-5.2"),
expected: func(val PathValue) bool {
return val.ValueString() == "1-5.2"
},
},
}
for name, test := range tests {
t.Run(
name, func(t *testing.T) {
t.Parallel()
ctx := context.TODO()
val, err := PathType{}.ValueFromTerraform(ctx, test.val)
if err == nil && test.expectError {
t.Fatal("expected error, got no error")
}
if err != nil && !test.expectError {
t.Fatalf("got unexpected error: %s", err)
}
if !test.expected(val.(PathValue)) {
t.Errorf("unexpected result")
}
},
)
}
}

View File

@ -0,0 +1,23 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package validators
import (
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/hardwaremapping"
)
const (
// HardwareMappingDeviceIDValidatorErrMessage is the error message when the validation fails.
HardwareMappingDeviceIDValidatorErrMessage = `value must be a valid hardware mapping device ID, e.g. "8086:5916"`
)
// HardwareMappingDeviceIDValidator validates a hardware mapping device ID.
func HardwareMappingDeviceIDValidator() validator.String {
return NewParseValidator(proxmoxtypes.ParseDeviceID, HardwareMappingDeviceIDValidatorErrMessage)
}

View File

@ -12,6 +12,7 @@ import (
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
clusterfirewall "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/firewall"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/mapping"
"github.com/bpg/terraform-provider-proxmox/proxmox/firewall"
)
@ -36,3 +37,8 @@ func (c *Client) Firewall() clusterfirewall.API {
func (c *Client) HA() *ha.Client {
return &ha.Client{Client: c}
}
// HardwareMapping returns a client for managing the cluster's hardware mapping features.
func (c *Client) HardwareMapping() *mapping.Client {
return &mapping.Client{Client: c}
}

View File

@ -0,0 +1,19 @@
### Create Proxmox VE API acsess ticket
POST {{host}}:{{port}}/{{api-path-base}}/access/ticket
Accept: application/json
Content-Type: application/x-www-form-urlencoded
username = {{api-user}} &
password = {{api-user-password}}
// Save the ticket into a global variable to use in other calls.
> {%
client.global.set("pve_api_access_ticket", response.body.data.ticket);
client.global.set("pve_api_access_ticket_csrf_prevention_token", response.body.data.CSRFPreventionToken);
client.log(`Received Proxmox VE API access ticket "${client.global.get("pve_api_access_ticket")}"`)
%}
### List HA resources
GET {{host}}:{{port}}/{{api-path-base}}/cluster/ha/resources
Accept: application/json
Cookie: PVEAuthCookie={{pve_api_access_ticket}}

View File

@ -0,0 +1,12 @@
{
"utm-8.1-1": {
"api-path-base": "api2/json",
"host": "https://172.16.1.10",
"port": "8006"
},
"adelie": {
"api-path-base": "api2/json",
"host": "https://192.168.178.30",
"port": "8006"
}
}

View File

@ -0,0 +1,10 @@
{
"utm-8.1-1": {
"api-user": "root@pam",
"api-user-password": "root123"
},
"adelie": {
"api-user": "root@pam",
"api-user-password": "root123"
}
}

View File

@ -0,0 +1,37 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package mapping
import (
"fmt"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/hardwaremapping"
)
// Client is an interface for accessing the Proxmox cluster mapping API.
type Client struct {
api.Client
}
func (c *Client) basePath() string {
return c.Client.ExpandPath("mapping")
}
// ExpandPath expands a relative path to a full hardware mapping API path.
func (c *Client) ExpandPath(hmType proxmoxtypes.Type, path string) string {
ep := c.basePath()
if hmType.String() != "" {
ep = fmt.Sprintf("%s/%s", ep, hmType.String())
}
if path != "" {
ep = fmt.Sprintf("%s/%s", ep, path)
}
return ep
}

View File

@ -0,0 +1,85 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package mapping
import (
"context"
"fmt"
"net/http"
"net/url"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/hardwaremapping"
)
// Create creates a new hardware mapping.
func (c *Client) Create(ctx context.Context, hmType proxmoxtypes.Type, data *CreateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath(hmType, ""), data, nil)
if err != nil {
return fmt.Errorf("creating hardware mapping %q: %w", data.ID, err)
}
return nil
}
// Delete deletes a hardware mapping.
func (c *Client) Delete(ctx context.Context, hmType proxmoxtypes.Type, name string) error {
err := c.DoRequest(ctx, http.MethodDelete, c.ExpandPath(hmType, url.PathEscape(name)), nil, nil)
if err != nil {
return fmt.Errorf("deleting hardware mapping %q: %w", name, err)
}
return nil
}
// Get retrieves the configuration of a single hardware mapping.
func (c *Client) Get(ctx context.Context, hmType proxmoxtypes.Type, name string) (*GetResponseData, error) {
resBody := &GetResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(hmType, url.PathEscape(name)), nil, resBody)
if err != nil {
return nil, fmt.Errorf("reading hardware mapping %q: %w", name, err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// List retrieves the list of hardware mappings.
// If "checkNode" is not empty, the "checks" list will be included in the response that might include configuration
// correctness diagnostics for the given node.
func (c *Client) List(ctx context.Context, hmType proxmoxtypes.Type, checkNode string) ([]*ListResponseData, error) {
options := &listQuery{
CheckNode: checkNode,
}
resBody := &ListResponseBody{}
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(hmType, ""), options, resBody)
if err != nil {
return nil, fmt.Errorf("listing hardware mapping: %w", err)
}
if resBody.Data == nil {
return nil, api.ErrNoDataObjectInResponse
}
return resBody.Data, nil
}
// Update updates an existing hardware mapping.
func (c *Client) Update(ctx context.Context, hmType proxmoxtypes.Type, name string, data *UpdateRequestBody) error {
err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath(hmType, url.PathEscape(name)), data, nil)
if err != nil {
return fmt.Errorf("udating hardware mapping %q: %w", name, err)
}
return nil
}

View File

@ -0,0 +1,110 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package mapping
import (
"github.com/bpg/terraform-provider-proxmox/proxmox/types"
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types/hardwaremapping"
)
const (
// APIParamNamePCIMediatedDevices is the API attribute name of the Proxmox VE API "mediated devices" parameter for a
// PCI hardware mapping.
APIParamNamePCIMediatedDevices = "mdev"
)
type listQuery struct {
// CheckNode is the name of the node those configuration should be checked for correctness.
CheckNode string `url:"check-node,omitempty"`
}
// DataBase contains common data for hardware mapping API calls.
type DataBase struct {
// Description is the optional key for the description for a hardware mapping that is omitted by the Proxmox VE API
// when not set.
// Note that even though the Proxmox VE API attribute is named "description" it is generally labeled as "comment"
// cross the Proxmox VE web UI while only being named "description" in the Proxmox VE API and its documentation.
Description *string `url:"description,omitempty"`
// Map is the list of device mappings.
Map []proxmoxtypes.Map `json:"map" url:"map"`
// MediatedDevices is the indicator for the optional APIParamNamePCIMediatedDevices parameter.
MediatedDevices types.CustomBool `json:"mdev" url:"mdev,omitempty,int"`
}
// CreateRequestBody contains the data which must be sent when creating a hardware mapping.
type CreateRequestBody struct {
DataBase
// ID is the hardware mappings identifier.
ID string `url:"id"`
}
// GetResponseBody contains the body from a hardware mapping get response.
type GetResponseBody struct {
// Data is the hardware mapping get response data.
Data *GetResponseData `json:"data,omitempty"`
}
// ListResponseBody contains the body from a hardware mapping list response.
type ListResponseBody struct {
// Data is the hardware mapping list response data.
Data []*ListResponseData `json:"data,omitempty"`
}
// GetResponseData contains data received from the hardware mapping API when requesting information about a single
// mapping.
type GetResponseData struct {
DataBase
// Type is the required types of the hardware mapping.
Type proxmoxtypes.Type `json:"type"`
}
// ListResponseData contains the data from a hardware mapping list response.
type ListResponseData struct {
DataBase
// ChecksPCI might contain relevant diagnostics about incorrect [typesHWM.TypePCI] configurations.
// The name of the node must be passed to the Proxmox VE API call which maps to the "check-node" URL parameter.
// Note that the Proxmox VE API, for whatever reason, only returns one error at a time, even though the field is an
// array.
ChecksPCI []NodeCheckDiag `json:"checks,omitempty"`
// ChecksUSB might contain relevant diagnostics about incorrect [typesHWM.TypeUSB] configurations.
// The name of the node must be passed to the Proxmox VE API call which maps to the "check-node" URL parameter.
// Note that the actual JSON field name matches the Proxmox VE API, but the name of this variable has been adjusted
// for clarity.
// Also note that the Proxmox VE API, for whatever reason, only returns one error at a time, even though the field is
// an array.
ChecksUSB []NodeCheckDiag `json:"errors,omitempty"`
// ID is the hardware mappings identifier.
ID string `json:"id"`
// Type is the required types of the hardware mapping.
Type proxmoxtypes.Type `json:"type"`
}
// NodeCheckDiag is a hardware mapping configuration correctness diagnostic entry.
type NodeCheckDiag struct {
// Message is the message of the node check diagnostic entry.
Message *string `json:"message"`
// Severity is the severity of the node check diagnostic entry.
Severity *string `json:"severity"`
}
// UpdateRequestBody contains data received from the hardware mapping resource API when updating an existing hardware
// mapping resource.
type UpdateRequestBody struct {
DataBase
// Delete are settings that must be deleted from the resource's configuration.
Delete []string `url:"delete,omitempty,comma"`
}

View File

@ -0,0 +1,110 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package hardwaremapping
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"regexp"
"github.com/google/go-querystring/query"
"github.com/hashicorp/terraform-plugin-framework/types"
)
const (
// attrNameDeviceID is the attribute name of the device ID in a hardware mapping.
attrNameDeviceID = "id"
// attrNameSubsystemID is the attribute name of the device subsystem ID in a hardware mapping.
attrNameSubsystemID = "subsystem-id"
)
// DeviceIDAttrValueRegEx is the regular expression for device ID attribute value in a hardware mapping.
var DeviceIDAttrValueRegEx = regexp.MustCompile(`^[0-9A-Fa-f]{4}:[0-9A-Fa-f]{4}$`)
// Ensure the hardware mapping device ID type implements required interfaces.
var (
_ fmt.Stringer = new(DeviceID)
_ json.Marshaler = new(DeviceID)
_ json.Unmarshaler = new(DeviceID)
_ query.Encoder = new(DeviceID)
)
// DeviceID represents a hardware mapping device ID.
// An ID is composed of two parts, either…
// - a Vendor ID and device ID.
// This is the device class and subclass (two 8-bit numbers).
// - Subsystem ID and Subsystem device ID.
// This identifies the assembly in which the device is contained.
// Subsystems have their vendor ID (from the same namespace as device vendors) and subsystem ID.
//
// References:
// - [Linux Kernel Documentation — PCI drivers]
// - [Linux Hardware Database]
// - [Linux USB ID Repository]
// - [man(5) — pci.ids]
//
// [Linux Kernel Documentation — PCI drivers]: https://docs.kernel.org/admin-guide/media/pci-cardlist.html
// [Linux Hardware Database]: https://linux-hardware.org
// [Linux USB ID Repository]: http://www.linux-usb.org/usb-ids.html
// [man(5) — pci.ids]: https://man.archlinux.org/man/core/pciutils/pci.ids.5.en#INTRODUCTION
type DeviceID string
// EncodeValues encodes a hardware mapping device ID field into a URL-encoded set of values.
func (did DeviceID) EncodeValues(key string, v *url.Values) error {
v.Add(key, did.String())
return nil
}
// MarshalJSON marshals a hardware mapping device ID into JSON value.
func (did DeviceID) MarshalJSON() ([]byte, error) {
bytes, err := json.Marshal(did)
if err != nil {
return nil, errors.Join(ErrDeviceIDMarshal, err)
}
return bytes, nil
}
// String converts a DeviceID value into a string.
func (did DeviceID) String() string {
return string(did)
}
// ToValue converts a hardware mapping device ID into a Terraform value.
func (did DeviceID) ToValue() types.String {
return types.StringValue(did.String())
}
// UnmarshalJSON unmarshals a hardware mapping device ID.
func (did *DeviceID) UnmarshalJSON(b []byte) error {
var pciMapID string
err := json.Unmarshal(b, &pciMapID)
if err != nil {
return errors.Join(ErrDeviceIDUnmarshal, err)
}
resType, err := ParseDeviceID(pciMapID)
if err == nil {
*did = resType
}
return err
}
// ParseDeviceID parses a string that represents a hardware mapping device ID into a DeviceID.
func ParseDeviceID(input string) (DeviceID, error) {
if !DeviceIDAttrValueRegEx.MatchString(input) {
return "", ErrDeviceIDParsing(input)
}
return DeviceID(input), nil
}

View File

@ -0,0 +1,69 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package hardwaremapping
import (
"fmt"
"github.com/hashicorp/terraform-plugin-framework/function"
)
//nolint:gochecknoglobals
var (
// ErrIDMarshal indicates an error while marshalling a hardware mapping ID.
ErrIDMarshal = function.NewFuncError("cannot unmarshal hardware mapping ID")
// ErrIDParsing indicates an error while parsing a hardware mapping ID.
ErrIDParsing = func(hmID string) error {
return function.NewFuncError(fmt.Sprintf("%q is not a valid hardware mapping ID", hmID))
}
// ErrDeviceIDMarshal indicates an error while marshalling a hardware mapping device ID.
ErrDeviceIDMarshal = function.NewFuncError("cannot marshal hardware mapping device ID")
// ErrDeviceIDParsing indicates an error while parsing a hardware mapping device ID.
ErrDeviceIDParsing = func(hmID string) error {
return function.NewFuncError(
fmt.Sprintf(
"invalid value %q for hardware mapping device ID attribute %q: no match for regular expression %q",
hmID,
attrNameDeviceID,
DeviceIDAttrValueRegEx.String(),
),
)
}
// ErrDeviceIDUnmarshal indicates an error while unmarshalling a hardware mapping device ID.
ErrDeviceIDUnmarshal = function.NewFuncError("cannot unmarshal hardware mapping device ID")
// ErrMapMarshal indicates an error while marshalling a hardware mapping.
ErrMapMarshal = function.NewFuncError("cannot marshal hardware mapping")
// ErrMapParsingFormat indicates an error the format of a hardware mapping while parsing.
ErrMapParsingFormat = func(format string, attrs ...any) error {
return function.NewFuncError(fmt.Sprintf(format, attrs...))
}
// ErrMapUnknownAttribute indicates an unknown hardware mapping attribute.
ErrMapUnknownAttribute = func(attr string) error {
return function.NewFuncError(fmt.Sprintf("unknown hardware mapping attribute %q", attr))
}
// ErrMapUnmarshal indicates an error while unmarshalling a hardware mapping.
ErrMapUnmarshal = function.NewFuncError("cannot unmarshal hardware mapping")
// ErrTypeIllegal indicates an error for an illegal hardware mapping type.
ErrTypeIllegal = func(hmTypeName string) error {
return function.NewFuncError(fmt.Sprintf("illegal hardware mapping type %q", hmTypeName))
}
// ErrTypeMarshal indicates an error while marshalling a hardware mapping type.
ErrTypeMarshal = function.NewFuncError("cannot marshal hardware mapping type")
// ErrTypeUnmarshal indicates an error while unmarshalling a hardware mapping type.
ErrTypeUnmarshal = function.NewFuncError("cannot unmarshal hardware mapping type")
)

View File

@ -0,0 +1,98 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package hardwaremapping
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"github.com/google/go-querystring/query"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// ID represents a hardware mapping ID, composed of the type and identifier.
type ID struct {
// Name is the name of the hardware mapping.
Name string
// Type is the type of the hardware mapping.
Type Type
}
// Ensure the hardware mapping ID type implements required interfaces.
var (
_ fmt.Stringer = &ID{}
_ json.Marshaler = &ID{}
_ json.Unmarshaler = &ID{}
_ query.Encoder = &ID{}
)
// EncodeValues encodes a hardware mapping ID field into a URL-encoded set of values.
func (hmid ID) EncodeValues(key string, v *url.Values) error {
v.Add(key, hmid.String())
return nil
}
// MarshalJSON marshals a hardware mapping ID into JSON value.
func (hmid ID) MarshalJSON() ([]byte, error) {
bytes, err := json.Marshal(hmid.String())
if err != nil {
return nil, errors.Join(ErrIDMarshal, err)
}
return bytes, nil
}
// String converts an ID value into a string.
func (hmid ID) String() string {
return fmt.Sprintf("%s:%s", hmid.Type, hmid.Name)
}
// ToValue converts a hardware mapping ID into a Terraform value.
func (hmid ID) ToValue() types.String {
return types.StringValue(hmid.String())
}
// UnmarshalJSON unmarshals a hardware mapping ID.
func (hmid *ID) UnmarshalJSON(b []byte) error {
var hmIDString string
err := json.Unmarshal(b, &hmIDString)
if err != nil {
return errors.Join(ErrTypeUnmarshal, err)
}
hmID, err := ParseID(hmIDString)
if err == nil {
*hmid = hmID
}
return err
}
// ParseID parses a string that represents a hardware mapping ID into a value of `ID`.
func ParseID(input string) (ID, error) {
hmID := ID{}
inParts := strings.SplitN(input, ":", 2)
if len(inParts) < 2 {
return hmID, ErrIDParsing(input)
}
hmType, err := ParseType(inParts[0])
if err != nil {
return hmID, errors.Join(fmt.Errorf("could not extract type from hardware mapping ID %q", input), err)
}
hmID.Type = hmType
hmID.Name = inParts[1]
return hmID, nil
}

View File

@ -0,0 +1,236 @@
/*
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package hardwaremapping
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"github.com/google/go-querystring/query"
"github.com/hashicorp/terraform-plugin-framework/types"
)
const (
// attrCountMax is the maximum number of attributes for a hardware mapping where only TypePCI can reach
// this limit.
attrCountMax = 6
// attrNameIOMMUGroup is the attribute key name of the IOMMU group in a hardware mapping.
attrNameIOMMUGroup = "iommugroup"
// attrNameNode is the attribute key name of the node in a hardware mapping.
attrNameNode = "node"
// attrNameNode is the attribute key name of the path in a hardware mapping.
attrNamePath = "path"
// attrSeparator is the separator for the attributes in a hardware mapping PCI map.
attrSeparator = ','
// attrValueSeparator is the separator for the attribute key-value pairs in a hardware mapping.
attrValueSeparator = '='
// AttrNameDescription is the attribute key name of the description in a hardware mapping.
// The Proxmox VE API attribute is named "description" while we name it "comment" internally since this naming is
// generally used across the Proxmox VE web UI and API documentations. This still follows the
// [Terraform "best practices"] as it improves the user experience by matching the field name to the naming used in
// the human-facing interfaces.
//
// [Terraform "best practices"]: https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles#resource-and-attribute-schema-should-closely-match-the-underlying-api
AttrNameDescription = "description"
)
// Ensure the hardware mapping type implements required interfaces.
var (
_ fmt.Stringer = &Map{}
_ json.Marshaler = &Map{}
_ json.Unmarshaler = &Map{}
_ query.Encoder = &Map{}
)
// Map represents a hardware mapping composed of multiple attributes.
type Map struct {
// Description is the optional "description" for a hardware mapping for both TypePCI and TypeUSB.
Description *string
// Description is the required "ID" for a hardware mapping for both TypePCI and TypeUSB.
ID DeviceID
// IOMMUGroup is the optional "IOMMU group" for a hardware mapping for TypePCI.
// The value is not mandatory for the Proxmox VE API, but causes a TypePCI to be incomplete when not set.
// It is not used for TypeUSB.
//
// Using a pointer is required to prevent the default value of 0 to be used as a valid IOMMU group but differentiate
// between and unset value instead.
//
// References:
// - [Proxmox VE Wiki — PCI Passthrough]
// - [Linux Kernel Documentations — VFIO - "Virtual Function I/O"]
// - [IOMMU DB]
//
// [Proxmox VE Wiki — PCI Passthrough]: https://pve.proxmox.com/wiki/PCI_Passthrough
// [Linux Kernel Documentations — VFIO - "Virtual Function I/O"]: https://docs.kernel.org/driver-api/vfio.html
// [IOMMU DB]: https://iommu.info
IOMMUGroup *int64
// Node is the required "node name" for a hardware mapping for both TypePCI and TypeUSB.
Node string
// Path is the "path" for a hardware mapping where this field is required for TypePCI but optional for
// TypeUSB.
Path *string
// SubsystemID is the optional "subsystem ID" for a hardware mapping for TypePCI.
// The value is not mandatory for the Proxmox VE API, but causes a TypePCI to be incomplete when not set.
// It is not used for TypeUSB.
SubsystemID DeviceID
}
// EncodeValues encodes a cluster mapping PCI map field into a URL-encoded set of values.
func (hm Map) EncodeValues(key string, v *url.Values) error {
v.Add(key, hm.String())
return nil
}
// MarshalJSON marshals a hardware mapping into JSON value.
func (hm Map) MarshalJSON() ([]byte, error) {
bytes, err := json.Marshal(hm.String())
if err != nil {
return nil, errors.Join(ErrMapMarshal, err)
}
return bytes, nil
}
// String converts a Map value into a string.
func (hm Map) String() string {
joinKV := func(k, v string) string {
return fmt.Sprintf("%s%s%s", k, string(attrValueSeparator), v)
}
attrs := make([]string, 0, attrCountMax)
attrs = append(
attrs,
joinKV(attrNameDeviceID, hm.ID.String()),
joinKV(attrNameNode, hm.Node),
)
if hm.Path != nil {
attrs = append(attrs, joinKV(attrNamePath, *hm.Path))
}
if hm.Description != nil {
attrs = append(attrs, joinKV(AttrNameDescription, *hm.Description))
}
if hm.IOMMUGroup != nil {
attrs = append(attrs, joinKV(attrNameIOMMUGroup, strconv.FormatInt(*hm.IOMMUGroup, 10)))
}
if hm.SubsystemID != "" {
attrs = append(attrs, joinKV(attrNameSubsystemID, hm.SubsystemID.String()))
}
return strings.Join(attrs, string(attrSeparator))
}
// ToValue converts a hardware mapping into a Terraform value.
func (hm Map) ToValue() types.String {
return types.StringValue(hm.String())
}
// UnmarshalJSON unmarshals a hardware mapping.
func (hm *Map) UnmarshalJSON(b []byte) error {
var hmString string
err := json.Unmarshal(b, &hmString)
if err != nil {
return errors.Join(ErrMapUnmarshal, err)
}
resType, err := ParseMap(hmString)
if err == nil {
*hm = resType
}
return err
}
// ParseMap parses a string that represents a hardware mapping into a Map.
func ParseMap(input string) (Map, error) {
hm := Map{}
// Scoped function to return an error when a regular expression for an attribute did not match.
regExNotMatchErr := func(attr, attrName string, err error) error {
return errors.Join(
ErrMapParsingFormat(
fmt.Sprintf(
"invalid format %q for hardware mapping %q attribute",
attr,
attrName,
),
), err,
)
}
// Split the full PCI map string into its attributes…
attrs := strings.Split(input, string(attrSeparator))
// …and iterate over each attribute to parse it into the struct fields.
for _, attr := range attrs {
attrSplit := strings.Split(attr, string(attrValueSeparator))
if len(attrSplit) != 2 {
return hm, ErrMapParsingFormat(
fmt.Sprintf(
`invalid "key=value" format for hardware mapping attribute %q`,
attr,
),
)
}
switch attrSplit[0] {
case AttrNameDescription:
hm.Description = &attrSplit[1]
case attrNameDeviceID:
id, err := ParseDeviceID(attrSplit[1])
if err != nil {
return hm, regExNotMatchErr(attrSplit[1], attrNameDeviceID, err)
}
hm.ID = id
case attrNameNode:
hm.Node = attrSplit[1]
case attrNamePath:
hm.Path = &attrSplit[1]
case attrNameIOMMUGroup:
iommuGroup, err := strconv.ParseInt(attrSplit[1], 10, 0)
if err != nil {
return hm, regExNotMatchErr(attrSplit[1], attrNameIOMMUGroup, err)
}
hm.IOMMUGroup = &iommuGroup
case attrNameSubsystemID:
subsystemID, err := ParseDeviceID(attrSplit[1])
if err != nil {
return hm, regExNotMatchErr(attrSplit[1], attrNameSubsystemID, err)
}
hm.SubsystemID = subsystemID
default:
return hm, ErrMapUnknownAttribute(attr)
}
}
return hm, nil
}

View File

@ -0,0 +1,91 @@
package hardwaremapping
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"github.com/google/go-querystring/query"
"github.com/hashicorp/terraform-plugin-framework/types"
)
//nolint:gochecknoglobals
var (
// TypePCI is an identifier for a PCI hardware mapping type.
// Do not modify this package-global variable as it acts as a safer variant compared to "iota" based constants!
TypePCI = Type{"pci"}
// TypeUSB is an identifier for a PCI hardware mapping type.
// Do not modify this package-global variable as it acts as a safer variant compared to "iota" based constants!
TypeUSB = Type{"usb"}
)
// Ensure the hardware mapping type supports required interfaces.
var (
_ fmt.Stringer = new(Type)
_ json.Marshaler = new(Type)
_ json.Unmarshaler = new(Type)
_ query.Encoder = new(Type)
)
// Type is the type of the hardware mapping.
type Type struct {
name string
}
// EncodeValues encodes a hardware mapping type field into a URL-encoded set of values.
func (t Type) EncodeValues(key string, v *url.Values) error {
v.Add(key, t.String())
return nil
}
// MarshalJSON marshals a hardware mapping type into JSON value.
func (t Type) MarshalJSON() ([]byte, error) {
bytes, err := json.Marshal(t.String())
if err != nil {
return nil, errors.Join(ErrTypeMarshal, err)
}
return bytes, nil
}
// String converts a Type value into a string.
func (t Type) String() string {
return t.name
}
// ToValue converts a hardware mapping type into a Terraform value.
func (t Type) ToValue() types.String {
return types.StringValue(t.String())
}
// UnmarshalJSON unmarshals a hardware mapping type.
func (t *Type) UnmarshalJSON(b []byte) error {
var rtString string
err := json.Unmarshal(b, &rtString)
if err != nil {
return errors.Join(ErrTypeUnmarshal, err)
}
resType, err := ParseType(rtString)
if err == nil {
*t = resType
}
return err
}
// ParseType converts the string representation of a hardware mapping type into the corresponding value.
// An error is returned if the input string does not match any known type.
func ParseType(input string) (Type, error) {
switch input {
case TypePCI.String():
return TypePCI, nil
case TypeUSB.String():
return TypeUSB, nil
default:
return Type{}, ErrTypeIllegal(input)
}
}

View File

@ -32,11 +32,16 @@ import (
//go:generate cp ../build/docs-gen/data-sources/virtual_environment_version.md ../docs/data-sources/
//go:generate cp ../build/docs-gen/data-sources/virtual_environment_hagroup.md ../docs/data-sources/
//go:generate cp ../build/docs-gen/data-sources/virtual_environment_hagroups.md ../docs/data-sources/
//go:generate cp ../build/docs-gen/data-sources/virtual_environment_hardware_mapping_pci.md ../docs/data-sources/
//go:generate cp ../build/docs-gen/data-sources/virtual_environment_hardware_mapping_usb.md ../docs/data-sources/
//go:generate cp ../build/docs-gen/data-sources/virtual_environment_hardware_mappings.md ../docs/data-sources/
//go:generate cp ../build/docs-gen/data-sources/virtual_environment_haresource.md ../docs/data-sources/
//go:generate cp ../build/docs-gen/data-sources/virtual_environment_haresources.md ../docs/data-sources/
//go:generate cp ../build/docs-gen/resources/virtual_environment_network_linux_bridge.md ../docs/resources/
//go:generate cp ../build/docs-gen/resources/virtual_environment_network_linux_vlan.md ../docs/resources/
//go:generate cp ../build/docs-gen/resources/virtual_environment_hagroup.md ../docs/resources/
//go:generate cp ../build/docs-gen/resources/virtual_environment_hardware_mapping_pci.md ../docs/resources/
//go:generate cp ../build/docs-gen/resources/virtual_environment_hardware_mapping_usb.md ../docs/resources/
//go:generate cp ../build/docs-gen/resources/virtual_environment_haresource.md ../docs/resources/
//go:generate cp ../build/docs-gen/resources/virtual_environment_cluster_options.md ../docs/resources/
//go:generate cp ../build/docs-gen/resources/virtual_environment_download_file.md ../docs/resources/