0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-06-29 18:21:10 +00:00

feat(hardwaremapping): Introduce support for directory mappings (#1902)

Signed-off-by: Fina Wilke <code@felinira.net>
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:
Fina 2025-04-17 00:54:22 +02:00 committed by GitHub
parent 6c447acb2e
commit 6dbff43bd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 974 additions and 57 deletions

View File

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

View File

@ -14,6 +14,11 @@ Retrieves a list of hardware mapping resources.
## Example Usage
```terraform
data "proxmox_virtual_environment_hardware_mappings" "example-dir" {
check_node = "pve"
type = "dir"
}
data "proxmox_virtual_environment_hardware_mappings" "example-pci" {
check_node = "pve"
type = "pci"

View File

@ -0,0 +1,62 @@
---
layout: page
title: proxmox_virtual_environment_hardware_mapping_dir
parent: Resources
subcategory: Virtual Environment
description: |-
Manages a directory mapping in a Proxmox VE cluster.
---
# Resource: proxmox_virtual_environment_hardware_mapping_dir
Manages a directory mapping in a Proxmox VE cluster.
## Example Usage
```terraform
resource "proxmox_virtual_environment_hardware_mapping_dir" "example" {
comment = "This is a comment"
name = "example"
# The actual map of devices.
map = [
{
node = "pve"
path = "/mnt/data"
},
]
}
```
<!-- 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 directory mapping.
### Optional
- `comment` (String) The comment of this directory mapping.
### Read-Only
- `id` (String) The unique identifier of this directory mapping resource.
<a id="nestedatt--map"></a>
### Nested Schema for `map`
Required:
- `node` (String) The node this mapping applies to.
- `path` (String) The path of the map. For directory mappings the path is required and refers to the POSIX path of the directory as visible from the node.
## Import
Import is supported using the following syntax:
```shell
#!/usr/bin/env sh
# A directory mapping can be imported using their name, e.g.:
terraform import proxmox_virtual_environment_hardware_mapping_dir.example example
```

View File

@ -240,6 +240,15 @@ resource "proxmox_virtual_environment_vm" "data_vm" {
}
}
resource "proxmox_virtual_environment_hardware_mapping_dir" "dir_mapping" {
name = "terraform-provider-proxmox-dir-mapping"
map = [{
node = data.proxmox_virtual_environment_nodes.example.names[0]
path = "/mnt"
}]
}
output "resource_proxmox_virtual_environment_vm_example_id" {
value = proxmox_virtual_environment_vm.example.id
}

View File

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

View File

@ -1,3 +1,8 @@
data "proxmox_virtual_environment_hardware_mappings" "example-dir" {
check_node = "pve"
type = "dir"
}
data "proxmox_virtual_environment_hardware_mappings" "example-pci" {
check_node = "pve"
type = "pci"

View File

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

View File

@ -0,0 +1,11 @@
resource "proxmox_virtual_environment_hardware_mapping_dir" "example" {
comment = "This is a comment"
name = "example"
# The actual map of devices.
map = [
{
node = "pve"
path = "/mnt/data"
},
]
}

View File

@ -111,10 +111,12 @@ func (d *dataSource) Read(ctx context.Context, req datasource.ReadRequest, resp
// 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 (len(data.ChecksPCI) > 0) || len(data.ChecksUSB) > 0 {
if (len(data.Checks) > 0) || len(data.ChecksUSB) > 0 {
switch data.Type {
case proxmoxtypes.TypeDir:
hm.Checks = append(hm.Checks, createCheckDiagnostics(data.ID, data.Checks)...)
case proxmoxtypes.TypePCI:
hm.Checks = append(hm.Checks, createCheckDiagnostics(data.ID, data.ChecksPCI)...)
hm.Checks = append(hm.Checks, createCheckDiagnostics(data.ID, data.Checks)...)
case proxmoxtypes.TypeUSB:
hm.Checks = append(hm.Checks, createCheckDiagnostics(data.ID, data.ChecksUSB)...)
}
@ -188,6 +190,7 @@ func (d *dataSource) Schema(
Validators: []validator.String{
stringvalidator.OneOf(
[]string{
proxmoxtypes.TypeDir.String(),
proxmoxtypes.TypePCI.String(),
proxmoxtypes.TypeUSB.String(),
}...,

View File

@ -0,0 +1,140 @@
/*
* 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/attribute"
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
customtypes "github.com/bpg/terraform-provider-proxmox/fwprovider/types/hardwaremapping"
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 = &dirDataSource{}
_ datasource.DataSourceWithConfigure = &dirDataSource{}
)
// dirDataSource is the data source implementation for a directory mapping.
type dirDataSource struct {
client *mappings.Client
}
// Configure adds the provider-configured client to the data source.
func (d *dirDataSource) Configure(
_ context.Context,
req datasource.ConfigureRequest,
resp *datasource.ConfigureResponse,
) {
if req.ProviderData == nil {
return
}
cfg, ok := req.ProviderData.(config.DataSource)
if !ok {
resp.Diagnostics.AddError(
"Unexpected DataSource Configure Type",
fmt.Sprintf("Expected config.DataSource, got: %T", req.ProviderData),
)
return
}
d.client = cfg.Client.Cluster().HardwareMapping()
}
// Metadata returns the data source type name.
func (d *dirDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_hardware_mapping_dir"
}
// Read fetches the specified directory mapping from the Proxmox VE API.
func (d *dirDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var hm modelDir
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.TypeDir, hmID)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Unable to read directory mapping %q", hmID),
err.Error(),
)
return
}
hm.importFromAPI(ctx, data)
resp.Diagnostics.Append(resp.State.Set(ctx, &hm)...)
}
// Schema defines the schema for the directory mapping.
func (d *dirDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
comment := dataSourceSchemaBaseAttrComment
comment.Optional = false
comment.Computed = true
comment.Description = "The comment of this directory mapping."
resp.Schema = schema.Schema{
Description: "Retrieves a directory 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 directory mapping.",
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
schemaAttrNameMapNode: schema.StringAttribute{
Computed: true,
Description: "The node name attribute of the map.",
},
schemaAttrNameMapPath: schema.StringAttribute{
// For directory mappings the path is required and refers
// to the POSIX path of the directory as visible from the node.
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 directory mapping.",
Required: true,
},
schemaAttrNameTerraformID: attribute.ResourceID(
"The unique identifier of this directory mapping data source.",
),
},
}
}
// NewDirDataSource returns a new data source for a directory mapping.
// This is a helper function to simplify the provider implementation.
func NewDirDataSource() datasource.DataSource {
return &dirDataSource{}
}

View File

@ -89,6 +89,15 @@ const (
schemaAttrNameHWMIDs = "ids"
)
// modelDirMap maps the schema data for the map of a directory mapping.
type modelDirMap struct {
// 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"`
}
// modelPCIMap maps the schema data for the map of a PCI hardware mapping.
type modelPCIMap struct {
// Comment is the "comment" for the map.
@ -140,6 +149,26 @@ type modelUSBMap struct {
Path customtypes.PathValue `tfsdk:"path"`
}
// modelDir maps the schema data for a directory mapping.
type modelDir struct {
// Comment is the comment of the directory 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 directory mapping.
Name types.String `tfsdk:"name"`
// Map is the map of the directory mapping.
Map []modelDirMap `tfsdk:"map"`
}
// modelPCI maps the schema data for a PCI hardware mapping.
type modelPCI struct {
// Comment is the comment of the PCI hardware mapping.
@ -223,6 +252,78 @@ type modelNodeCheckDiag struct {
Severity types.String `tfsdk:"severity"`
}
// importFromAPI imports the contents of a directory mapping model from the Proxmox VE API's response data.
func (hm *modelDir) 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([]modelDirMap, len(data.Map))
for idx, pveMap := range data.Map {
tfMap := modelDirMap{
Node: types.StringValue(pveMap.Node),
Path: customtypes.NewPathPointerValue(pveMap.Path),
}
maps[idx] = tfMap
}
hm.Map = maps
}
// toCreateRequest builds the request data structure for creating a new directory mapping.
func (hm *modelDir) toCreateRequest() *apitypes.CreateRequestBody {
return &apitypes.CreateRequestBody{
DataBase: hm.toRequestBase(),
ID: hm.ID.ValueString(),
}
}
// toRequestBase builds the common request data structure for the directory mapping creation or update API calls.
func (hm *modelDir) 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{
Node: tfMap.Node.ValueString(),
Path: tfMap.Path.ValueStringPointer(),
}
maps[idx] = pveMap
}
dataBase.Map = maps
return dataBase
}
// toUpdateRequest builds the request data structure for updating an existing USB hardware mapping.
func (hm *modelDir) toUpdateRequest(currentState *modelDir) *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,
}
}
// 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.

View File

@ -0,0 +1,280 @@
/*
* 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/attribute"
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
customtypes "github.com/bpg/terraform-provider-proxmox/fwprovider/types/hardwaremapping"
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 = &dirResource{}
_ resource.ResourceWithConfigure = &dirResource{}
_ resource.ResourceWithImportState = &dirResource{}
)
// dirResource contains the directory mapping resource's internal data.
type dirResource struct {
// client is the hardware mapping API client.
client *mappings.Client
}
// read reads information about a directory mapping from the Proxmox VE API.
func (r *dirResource) read(ctx context.Context, hm *modelDir) (bool, diag.Diagnostics) {
var diags diag.Diagnostics
hmName := hm.Name.ValueString()
data, err := r.client.Get(ctx, proxmoxtypes.TypeDir, hmName)
if err != nil {
if strings.Contains(err.Error(), "no such resource") {
diags.AddError("Could not read directory mapping", err.Error())
}
return false, diags
}
hm.importFromAPI(ctx, data)
return true, nil
}
// readBack reads information about a created or modified directory 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 *dirResource) readBack(ctx context.Context, hm *modelDir, respDiags *diag.Diagnostics, respState *tfsdk.State) {
found, diags := r.read(ctx, hm)
respDiags.Append(diags...)
if !found {
respDiags.AddError(
"directory mapping resource not found after update",
"Failed to find the resource when trying to read back the updated directory mapping's data.",
)
}
if !respDiags.HasError() {
respDiags.Append(respState.Set(ctx, *hm)...)
}
}
// Configure adds the provider-configured client to the resource.
func (r *dirResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
cfg, ok := req.ProviderData.(config.Resource)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected config.Resource, got: %T", req.ProviderData),
)
return
}
r.client = cfg.Client.Cluster().HardwareMapping()
}
// Create creates a new directory mapping.
func (r *dirResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var hm modelDir
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.TypeDir, apiReq); err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Could not create directory mapping %q.", hmName),
err.Error(),
)
return
}
r.readBack(ctx, &hm, &resp.Diagnostics, &resp.State)
}
// Delete deletes an existing directory mapping.
func (r *dirResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var hm modelDir
resp.Diagnostics.Append(req.State.Get(ctx, &hm)...)
if resp.Diagnostics.HasError() {
return
}
hmID := hm.Name.ValueString()
if err := r.client.Delete(ctx, proxmoxtypes.TypeDir, hmID); err != nil {
if strings.Contains(err.Error(), "no such resource") {
resp.Diagnostics.AddWarning(
"directory mapping does not exist",
fmt.Sprintf(
"Could not delete directory mapping %q, it does not exist or has been deleted outside of Terraform.",
hmID,
),
)
} else {
resp.Diagnostics.AddError(fmt.Sprintf("Could not delete directory mapping %q.", hmID), err.Error())
}
}
}
// ImportState imports a directory mapping from the Proxmox VE API.
func (r *dirResource) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
data := modelDir{
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 directory mapping.
func (r *dirResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_hardware_mapping_dir"
}
// Read reads the directory mapping.
//
func (r *dirResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data modelDir
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 directory mapping.
func (r *dirResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
comment := resourceSchemaBaseAttrComment
comment.Description = "The comment of this directory mapping."
resp.Schema = schema.Schema{
Description: "Manages a directory 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{
schemaAttrNameMapNode: schema.StringAttribute{
Description: "The node this mapping applies to.",
Required: true,
},
schemaAttrNameMapPath: schema.StringAttribute{
CustomType: customtypes.PathType{},
Description: "The path of the map. For directory mappings the path is required and refers" +
" to the POSIX path of the directory as visible from the node.",
Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(
customtypes.PathDirValueRegEx,
ErrResourceMessageInvalidPath(proxmoxtypes.TypeDir),
),
},
},
},
},
Required: true,
Validators: []validator.Set{
setvalidator.SizeAtLeast(1),
},
},
schemaAttrNameName: schema.StringAttribute{
Description: "The name of this directory mapping.",
Required: true,
},
schemaAttrNameTerraformID: attribute.ResourceID(
"The unique identifier of this directory mapping resource.",
),
},
}
}
// Update updates an existing directory mapping.
func (r *dirResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var hmCurrent, hmPlan modelDir
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.TypeDir, hmName, apiReq); err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Could not update directory mapping %q.", hmName),
err.Error(),
)
return
}
r.readBack(ctx, &hmPlan, &resp.Diagnostics, &resp.State)
}
// NewDirResource returns a new resource for managing a directory mapping.
// This is a helper function to simplify the provider implementation.
func NewDirResource() resource.Resource {
return &dirResource{}
}

View File

@ -27,15 +27,18 @@ import (
)
const (
accTestHardwareMappingNameDir = "proxmox_virtual_environment_hardware_mapping_dir.test"
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"`
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"`
// These paths must exist on the host system, use a hardcoded list
MapPathsDir []string `fake:"{randomstring:[/home,/root,/mnt,/tmp]}" fakesize:"2"`
MapPathsPCI []string `fake:"{linuxdevicepathpci}" fakesize:"2"`
MapPathsUSB []string `fake:"{linuxdevicepathusb}" fakesize:"2"`
MapSubsystemIDs []string `fake:"{linuxdeviceid}" fakesize:"2"`
@ -94,6 +97,226 @@ func testAccResourceHardwareMappingInit(t *testing.T) (*accTestHardwareMappingFa
return &data, te
}
// TestAccResourceHardwareMappingDirValidInput runs tests for directory 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 TestAccResourceHardwareMappingDirValidInput(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_dir" "test" {
comment = "%s"
name = "%s"
map = [
{
node = "%s"
path = "%s"
},
]
}
`,
data.Comments[0],
data.Names[0],
te.NodeName,
data.MapPathsDir[0],
),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(accTestHardwareMappingNameDir, "comment", data.Comments[0]),
resource.TestCheckResourceAttrSet(accTestHardwareMappingNameDir, "id"),
resource.TestCheckTypeSetElemNestedAttrs(
accTestHardwareMappingNameDir, "map.*", map[string]string{
"node": te.NodeName,
"path": data.MapPathsDir[0],
},
),
resource.TestCheckResourceAttr(accTestHardwareMappingNameDir, "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: accTestHardwareMappingNameDir,
},
// Test the "Update" implementation where all possible attributes are specified.
{
Config: fmt.Sprintf(
`
resource "proxmox_virtual_environment_hardware_mapping_dir" "test" {
comment = "%s"
name = "%s"
map = [
{
node = "%s"
path = "%s"
},
]
}
`,
data.Comments[1],
data.Names[0],
te.NodeName,
data.MapPathsDir[1],
),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(accTestHardwareMappingNameDir, "comment", data.Comments[1]),
resource.TestCheckResourceAttrSet(accTestHardwareMappingNameDir, "id"),
resource.TestCheckTypeSetElemNestedAttrs(
accTestHardwareMappingNameDir, "map.*", map[string]string{
"node": te.NodeName,
"path": data.MapPathsDir[1],
},
),
resource.TestCheckResourceAttr(accTestHardwareMappingNameDir, "name", data.Names[0]),
),
},
},
},
)
}
// TestAccResourceHardwareMappingDirValidInputMinimal runs tests for directory 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 TestAccResourceHardwareMappingDirValidInputMinimal(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_dir" "test" {
name = "%s"
map = [
{
node = "%s"
path = "%s"
},
]
}
`,
data.Names[0],
te.NodeName,
data.MapPathsDir[0],
),
ConfigStateChecks: []statecheck.StateCheck{
// Optional attributes should all be unset.
statecheck.ExpectKnownValue(accTestHardwareMappingNameDir,
tfjsonpath.New("comment"),
knownvalue.Null()),
},
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(accTestHardwareMappingNameDir, "id"),
resource.TestCheckTypeSetElemNestedAttrs(
accTestHardwareMappingNameDir, "map.*", map[string]string{
"node": te.NodeName,
"path": data.MapPathsDir[0],
},
),
resource.TestCheckResourceAttr(accTestHardwareMappingNameDir, "name", data.Names[0]),
),
},
// Test the "Update" implementation by setting all previously undefined attributes.
{
Config: fmt.Sprintf(
`
resource "proxmox_virtual_environment_hardware_mapping_dir" "test" {
comment = "%s"
name = "%s"
map = [
{
node = "%s"
path = "%s"
},
]
}
`,
data.Comments[0],
data.Names[0],
te.NodeName,
data.MapPathsDir[0],
),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(accTestHardwareMappingNameDir, "comment", data.Comments[0]),
resource.TestCheckResourceAttrSet(accTestHardwareMappingNameDir, "id"),
resource.TestCheckTypeSetElemNestedAttrs(
accTestHardwareMappingNameDir, "map.*", map[string]string{
"node": te.NodeName,
"path": data.MapPathsDir[0],
},
),
resource.TestCheckResourceAttr(accTestHardwareMappingNameDir, "name", data.Names[0]),
),
},
},
},
)
}
// TestAccResourceHardwareMappingDirInvalidInput runs tests for directory 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 TestAccResourceHardwareMappingDirInvalidInput(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.
{
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,
),
ExpectError: regexp.MustCompile(`valid Linux device path for hardware mapping of type "usb"`),
},
},
},
)
}
// TestAccResourceHardwareMappingPCIValidInput runs tests for PCI hardware mapping resource definitions with valid input
// where all possible attributes are
// specified.

View File

@ -506,6 +506,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc
apt.NewStandardRepositoryResource,
ha.NewHAGroupResource,
ha.NewHAResourceResource,
hardwaremapping.NewDirResource,
hardwaremapping.NewPCIResource,
hardwaremapping.NewUSBResource,
metrics.NewMetricsServerResource,
@ -532,6 +533,7 @@ func (p *proxmoxProvider) DataSources(_ context.Context) []func() datasource.Dat
ha.NewHAResourceDataSource,
ha.NewHAResourcesDataSource,
hardwaremapping.NewDataSource,
hardwaremapping.NewDirDataSource,
hardwaremapping.NewPCIDataSource,
hardwaremapping.NewUSBDataSource,
metrics.NewMetricsServerDatasource,

View File

@ -9,9 +9,11 @@
package test
import (
"fmt"
"regexp"
"testing"
"github.com/brianvoe/gofakeit/v7"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/plancheck"
@ -22,6 +24,10 @@ func TestAccResourceVM(t *testing.T) {
t.Parallel()
te := InitEnvironment(t)
dirName := fmt.Sprintf("dir_%s", gofakeit.Word())
te.AddTemplateVars(map[string]interface{}{
"DirName": dirName,
})
tests := []struct {
name string
@ -396,51 +402,50 @@ func TestAccResourceVM(t *testing.T) {
),
},
}},
// Depends on #1902
// {"create virtiofs block", []resource.TestStep{
// {
// Config: te.RenderConfig(`
// resource "proxmox_virtual_environment_hardware_mapping_dir" "test" {
// name = "test"
{"create virtiofs block", []resource.TestStep{
{
Config: te.RenderConfig(`
resource "proxmox_virtual_environment_hardware_mapping_dir" "test" {
name = "{{.DirName}}"
// map {
// node = "{{.NodeName}}"
// path = "/mnt"
// }
// }`, WithRootUser()),
// Check: resource.ComposeTestCheckFunc(
// ResourceAttributes("proxmox_virtual_environment_hardware_mapping_dir.test", map[string]string{
// "name": "test",
// "map.0.node": "{{.NodeName}}",
// "map.0.path": "/mnt",
// }),
// ),
// },
// {
// Config: te.RenderConfig(`
// resource "proxmox_virtual_environment_vm" "test_vm" {
// node_name = "{{.NodeName}}"
// started = false
map = [{
node = "{{.NodeName}}"
path = "/mnt"
}]
}`, WithRootUser()),
Check: resource.ComposeTestCheckFunc(
ResourceAttributes("proxmox_virtual_environment_hardware_mapping_dir.test", map[string]string{
"name": dirName,
"map.0.node": te.NodeName,
"map.0.path": "/mnt",
}),
),
},
{
Config: te.RenderConfig(`
resource "proxmox_virtual_environment_vm" "test_vm" {
node_name = "{{.NodeName}}"
started = false
// virtiofs {
// mapping = "test"
// cache = "always"
// direct_io = true
// expose_acl = false
// expose_xattr = false
// }
// }`, WithRootUser()),
// Check: resource.ComposeTestCheckFunc(
// ResourceAttributes("proxmox_virtual_environment_vm.test_vm", map[string]string{
// "virtiofs.0.mapping": "test",
// "virtiofs.0.cache": "always",
// "virtiofs.0.direct_io": "true",
// "virtiofs.0.expose_acl": "false",
// "virtiofs.0.expose_xattr": "false",
// }),
// ),
// },
// }},
virtiofs {
mapping = "{{.DirName}}"
cache = "always"
direct_io = true
expose_acl = false
expose_xattr = false
}
}`, WithRootUser()),
Check: resource.ComposeTestCheckFunc(
ResourceAttributes("proxmox_virtual_environment_vm.test_vm", map[string]string{
"virtiofs.0.mapping": dirName,
"virtiofs.0.cache": "always",
"virtiofs.0.direct_io": "true",
"virtiofs.0.expose_acl": "false",
"virtiofs.0.expose_xattr": "false",
}),
),
},
}},
}
for _, tt := range tests {

View File

@ -37,6 +37,9 @@ var ErrValueConversion = func(format string, attrs ...any) error {
}
var (
// PathDirValueRegEx is the regular expression for a POSIX path.
PathDirValueRegEx = regexp.MustCompile(`^/.+$`)
// 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])?$`)
@ -128,6 +131,8 @@ func (v PathValue) Equal(o attr.Value) bool {
// IsProxmoxType checks whether the value match the given hardware mapping type.
func (v PathValue) IsProxmoxType(hmType proxmoxtypes.Type) bool {
switch hmType {
case proxmoxtypes.TypeDir:
return PathDirValueRegEx.MatchString(v.ValueString())
case proxmoxtypes.TypePCI:
return PathPCIValueRegEx.MatchString(v.ValueString())
case proxmoxtypes.TypeUSB:

View File

@ -42,6 +42,7 @@ import (
//go:generate cp ./build/docs-gen/data-sources/virtual_environment_datastores.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_dir.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/
@ -58,6 +59,7 @@ import (
//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/
//go:generate cp ./build/docs-gen/resources/virtual_environment_hagroup.md ./docs/resources/
//go:generate cp ./build/docs-gen/resources/virtual_environment_hardware_mapping_dir.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/

View File

@ -70,11 +70,11 @@ type GetResponseData struct {
type ListResponseData struct {
DataBase
// ChecksPCI might contain relevant diagnostics about incorrect [typesHWM.TypePCI] configurations.
// Checks 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"`
Checks []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.

View File

@ -116,11 +116,14 @@ func (hm Map) 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),
)
// ID is optional for directory mappings
if hm.ID != "" {
attrs = append(attrs, joinKV(attrNameDeviceID, hm.ID.String()))
}
// Node is common among all mappings
attrs = append(attrs, joinKV(attrNameNode, hm.Node))
if hm.Path != nil {
attrs = append(attrs, joinKV(attrNamePath, *hm.Path))

View File

@ -12,11 +12,15 @@ import (
//nolint:gochecknoglobals
var (
// TypeDir is an identifier for a directory mapping type.
// Do not modify this package-global variable as it acts as a safer variant compared to "iota" based constants!
TypeDir = Type{"dir"}
// 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.
// TypeUSB is an identifier for a USB 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"}
)
@ -81,6 +85,8 @@ func (t *Type) UnmarshalJSON(b []byte) error {
// An error is returned if the input string does not match any known type.
func ParseType(input string) (Type, error) {
switch input {
case TypeDir.String():
return TypeDir, nil
case TypePCI.String():
return TypePCI, nil
case TypeUSB.String():