mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-06-29 18:21:10 +00:00
484 lines
14 KiB
Go
484 lines
14 KiB
Go
/*
|
|
* 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 fwprovider
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
|
|
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
|
|
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
|
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
|
"github.com/hashicorp/terraform-plugin-framework/provider"
|
|
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
|
|
|
"github.com/bpg/terraform-provider-proxmox/proxmox"
|
|
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
|
"github.com/bpg/terraform-provider-proxmox/proxmox/nodes"
|
|
"github.com/bpg/terraform-provider-proxmox/proxmox/ssh"
|
|
"github.com/bpg/terraform-provider-proxmox/utils"
|
|
)
|
|
|
|
// Ensure the implementation satisfies the expected interfaces.
|
|
var _ provider.Provider = &proxmoxProvider{}
|
|
|
|
// New is a helper function to simplify provider server and testing implementation.
|
|
func New(version string) func() provider.Provider {
|
|
return func() provider.Provider {
|
|
return &proxmoxProvider{
|
|
version: version,
|
|
}
|
|
}
|
|
}
|
|
|
|
type proxmoxProvider struct {
|
|
// version is set to the provider version on release, "dev" when the
|
|
// provider is built and ran locally, and "test" when running acceptance
|
|
// testing.
|
|
version string
|
|
}
|
|
|
|
// proxmoxProviderModel maps provider schema data.
|
|
type proxmoxProviderModel struct {
|
|
APIToken types.String `tfsdk:"api_token"`
|
|
Endpoint types.String `tfsdk:"endpoint"`
|
|
Insecure types.Bool `tfsdk:"insecure"`
|
|
OTP types.String `tfsdk:"otp"`
|
|
Username types.String `tfsdk:"username"`
|
|
Password types.String `tfsdk:"password"`
|
|
SSH []struct {
|
|
Agent types.Bool `tfsdk:"agent"`
|
|
AgentSocket types.String `tfsdk:"agent_socket"`
|
|
Password types.String `tfsdk:"password"`
|
|
Username types.String `tfsdk:"username"`
|
|
|
|
Nodes []struct {
|
|
Name types.String `tfsdk:"name"`
|
|
Address types.String `tfsdk:"address"`
|
|
Port types.Int64 `tfsdk:"port"`
|
|
} `tfsdk:"node"`
|
|
} `tfsdk:"ssh"`
|
|
TmpDir types.String `tfsdk:"tmp_dir"`
|
|
}
|
|
|
|
func (p *proxmoxProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
|
|
// resp.TypeName = "proxmox"
|
|
resp.TypeName = "proxmox_virtual_environment"
|
|
resp.Version = p.version
|
|
}
|
|
|
|
func (p *proxmoxProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
|
|
resp.Schema = schema.Schema{
|
|
// Attributes specified in alphabetical order.
|
|
Attributes: map[string]schema.Attribute{
|
|
"api_token": schema.StringAttribute{
|
|
Description: "The API token for the Proxmox VE API.",
|
|
Optional: true,
|
|
Sensitive: true,
|
|
Validators: []validator.String{
|
|
stringvalidator.RegexMatches(
|
|
regexp.MustCompile(`^\S+@\w+!\S+=([a-zA-Z0-9-]+)$`),
|
|
`must be a valid API token, e.g. 'USER@REALM!TOKENID=UUID'`,
|
|
),
|
|
},
|
|
},
|
|
"endpoint": schema.StringAttribute{
|
|
Description: "The endpoint for the Proxmox VE API.",
|
|
Optional: true,
|
|
Validators: []validator.String{
|
|
stringvalidator.LengthAtLeast(1),
|
|
},
|
|
},
|
|
"insecure": schema.BoolAttribute{
|
|
Description: "Whether to skip the TLS verification step.",
|
|
Optional: true,
|
|
},
|
|
"otp": schema.StringAttribute{
|
|
Description: "The one-time password for the Proxmox VE API.",
|
|
Optional: true,
|
|
DeprecationMessage: "The `otp` attribute is deprecated and will be removed in a future release. " +
|
|
"Please use the `api_token` attribute instead.",
|
|
},
|
|
"password": schema.StringAttribute{
|
|
Description: "The password for the Proxmox VE API.",
|
|
Optional: true,
|
|
Sensitive: true,
|
|
},
|
|
"username": schema.StringAttribute{
|
|
Description: "The username for the Proxmox VE API.",
|
|
Optional: true,
|
|
},
|
|
"tmp_dir": schema.StringAttribute{
|
|
Description: "The alternative temporary directory.",
|
|
Optional: true,
|
|
},
|
|
},
|
|
Blocks: map[string]schema.Block{
|
|
// have to define it as a list due to backwards compatibility
|
|
"ssh": schema.ListNestedBlock{
|
|
Description: "The SSH configuration for the Proxmox nodes.",
|
|
Validators: []validator.List{
|
|
listvalidator.SizeAtMost(1),
|
|
},
|
|
NestedObject: schema.NestedBlockObject{
|
|
Attributes: map[string]schema.Attribute{
|
|
"agent": schema.BoolAttribute{
|
|
Description: "Whether to use the SSH agent for authentication. " +
|
|
"Defaults to `false`.",
|
|
Optional: true,
|
|
},
|
|
"agent_socket": schema.StringAttribute{
|
|
Description: "The path to the SSH agent socket. " +
|
|
"Defaults to the value of the `SSH_AUTH_SOCK` " +
|
|
"environment variable.",
|
|
Optional: true,
|
|
},
|
|
"password": schema.StringAttribute{
|
|
Description: "The password used for the SSH connection. " +
|
|
"Defaults to the value of the `password` field of the " +
|
|
"`provider` block.",
|
|
Optional: true,
|
|
Sensitive: true,
|
|
},
|
|
"username": schema.StringAttribute{
|
|
Description: "The username used for the SSH connection. " +
|
|
"Defaults to the value of the `username` field of the " +
|
|
"`provider` block.",
|
|
Optional: true,
|
|
},
|
|
},
|
|
Blocks: map[string]schema.Block{
|
|
"node": schema.ListNestedBlock{
|
|
Description: "Overrides for SSH connection configuration for a Proxmox VE node.",
|
|
NestedObject: schema.NestedBlockObject{
|
|
Attributes: map[string]schema.Attribute{
|
|
"name": schema.StringAttribute{
|
|
Description: "The name of the Proxmox VE node.",
|
|
Required: true,
|
|
},
|
|
"address": schema.StringAttribute{
|
|
Description: "The address of the Proxmox VE node.",
|
|
Required: true,
|
|
},
|
|
"port": schema.Int64Attribute{
|
|
Description: "The port of the Proxmox VE node.",
|
|
Optional: true,
|
|
Validators: []validator.Int64{int64validator.Between(1, 65535)},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (p *proxmoxProvider) Configure(
|
|
ctx context.Context,
|
|
req provider.ConfigureRequest,
|
|
resp *provider.ConfigureResponse,
|
|
) {
|
|
tflog.Info(ctx, "Configuring the Proxmox provider...")
|
|
|
|
// Retrieve provider data from configuration
|
|
var config proxmoxProviderModel
|
|
diags := req.Config.Get(ctx, &config)
|
|
resp.Diagnostics.Append(diags...)
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// If practitioner provided a configuration value for any of the
|
|
// attributes, it must be a known value.
|
|
|
|
if config.Endpoint.IsUnknown() {
|
|
resp.Diagnostics.AddAttributeError(
|
|
path.Root("endpoint"),
|
|
"Unknown Proxmox VE API Endpoint",
|
|
"The provider cannot create the Proxmox VE API client as there is an unknown configuration value "+
|
|
"for the API endpoint. Either target apply the source of the value first, set the value statically in "+
|
|
"the configuration, or use the PROXMOX_VE_ENDPOINT environment variable.",
|
|
)
|
|
}
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// Default values to environment variables, but override
|
|
// with Terraform configuration value if set.
|
|
|
|
// Check environment variables
|
|
apiToken := utils.GetAnyStringEnv("PROXMOX_VE_API_TOKEN")
|
|
endpoint := utils.GetAnyStringEnv("PROXMOX_VE_ENDPOINT")
|
|
insecure := utils.GetAnyBoolEnv("PROXMOX_VE_INSECURE")
|
|
username := utils.GetAnyStringEnv("PROXMOX_VE_USERNAME")
|
|
password := utils.GetAnyStringEnv("PROXMOX_VE_PASSWORD")
|
|
|
|
if !config.APIToken.IsNull() {
|
|
apiToken = config.APIToken.ValueString()
|
|
}
|
|
|
|
if !config.Endpoint.IsNull() {
|
|
endpoint = config.Endpoint.ValueString()
|
|
}
|
|
|
|
if !config.Insecure.IsNull() {
|
|
insecure = config.Insecure.ValueBool()
|
|
}
|
|
|
|
if !config.Username.IsNull() {
|
|
username = config.Username.ValueString()
|
|
}
|
|
|
|
if !config.Password.IsNull() {
|
|
password = config.Password.ValueString()
|
|
}
|
|
|
|
if endpoint == "" {
|
|
resp.Diagnostics.AddAttributeError(
|
|
path.Root("endpoint"),
|
|
"Missing Proxmox VE API Endpoint",
|
|
"The provider cannot create the Proxmox VE API client as there is a missing or empty value for the API endpoint. "+
|
|
"Set the host value in the configuration or use the PROXMOX_VE_ENDPOINT environment variable. "+
|
|
"If either is already set, ensure the value is not empty.",
|
|
)
|
|
}
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// Create the Proxmox VE API client
|
|
|
|
creds, err := api.NewCredentials(username, password, "", apiToken)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Unable to create Proxmox VE API credentials",
|
|
err.Error(),
|
|
)
|
|
}
|
|
|
|
conn, err := api.NewConnection(
|
|
endpoint,
|
|
insecure,
|
|
)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Unable to create Proxmox VE API connection",
|
|
err.Error(),
|
|
)
|
|
}
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
apiClient, err := api.NewClient(creds, conn)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Unable to create Proxmox VE API client",
|
|
err.Error(),
|
|
)
|
|
}
|
|
|
|
sshUsername := utils.GetAnyStringEnv("PROXMOX_VE_SSH_USERNAME")
|
|
sshPassword := utils.GetAnyStringEnv("PROXMOX_VE_SSH_PASSWORD")
|
|
sshAgent := utils.GetAnyBoolEnv("PROXMOX_VE_SSH_AGENT")
|
|
sshAgentSocket := utils.GetAnyStringEnv("SSH_AUTH_SOCK", "PROXMOX_VE_SSH_AUTH_SOCK")
|
|
nodeOverrides := map[string]ssh.ProxmoxNode{}
|
|
|
|
//nolint: nestif
|
|
if len(config.SSH) > 0 {
|
|
if !config.SSH[0].Username.IsNull() {
|
|
sshUsername = config.SSH[0].Username.ValueString()
|
|
}
|
|
|
|
if !config.SSH[0].Password.IsNull() {
|
|
sshPassword = config.SSH[0].Password.ValueString()
|
|
}
|
|
|
|
if !config.SSH[0].Agent.IsNull() {
|
|
sshAgent = config.SSH[0].Agent.ValueBool()
|
|
}
|
|
|
|
if !config.SSH[0].AgentSocket.IsNull() {
|
|
sshAgentSocket = config.SSH[0].AgentSocket.ValueString()
|
|
}
|
|
|
|
for _, n := range config.SSH[0].Nodes {
|
|
nodePort := int32(n.Port.ValueInt64())
|
|
if nodePort == 0 {
|
|
nodePort = 22
|
|
}
|
|
|
|
nodeOverrides[n.Name.ValueString()] = ssh.ProxmoxNode{
|
|
Address: n.Address.ValueString(),
|
|
Port: nodePort,
|
|
}
|
|
}
|
|
}
|
|
|
|
if sshUsername == "" {
|
|
sshUsername = strings.Split(creds.Username, "@")[0]
|
|
}
|
|
|
|
if sshPassword == "" {
|
|
sshPassword = creds.Password
|
|
}
|
|
|
|
sshClient, err := ssh.NewClient(
|
|
sshUsername, sshPassword, sshAgent, sshAgentSocket,
|
|
&apiResolverWithOverrides{
|
|
ar: apiResolver{c: apiClient},
|
|
overrides: nodeOverrides,
|
|
},
|
|
)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Unable to create Proxmox VE SSH client",
|
|
err.Error(),
|
|
)
|
|
}
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// Intentionally use 'PROXMOX_VE_TMPDIR' with 'TMP' instead of 'TEMP', to match os.TempDir's use of $TMPDIR
|
|
tmpDirOverride := utils.GetAnyStringEnv("PROXMOX_VE_TMPDIR", "PM_VE_TMPDIR")
|
|
|
|
if !config.TmpDir.IsNull() {
|
|
tmpDirOverride = config.TmpDir.ValueString()
|
|
}
|
|
|
|
client := proxmox.NewClient(apiClient, sshClient, tmpDirOverride)
|
|
|
|
resp.ResourceData = client
|
|
resp.DataSourceData = client
|
|
}
|
|
|
|
func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resource {
|
|
return []func() resource.Resource{
|
|
NewHAGroupResource,
|
|
NewHAResourceResource,
|
|
NewClusterOptionsResource,
|
|
NewLinuxBridgeResource,
|
|
NewLinuxVLANResource,
|
|
}
|
|
}
|
|
|
|
func (p *proxmoxProvider) DataSources(_ context.Context) []func() datasource.DataSource {
|
|
return []func() datasource.DataSource{
|
|
NewVersionDataSource,
|
|
NewHAGroupsDataSource,
|
|
NewHAGroupDataSource,
|
|
NewHAResourcesDataSource,
|
|
NewHAResourceDataSource,
|
|
}
|
|
}
|
|
|
|
type apiResolver struct {
|
|
c api.Client
|
|
}
|
|
|
|
func (r *apiResolver) Resolve(ctx context.Context, nodeName string) (ssh.ProxmoxNode, error) {
|
|
nc := &nodes.Client{Client: r.c, NodeName: nodeName}
|
|
|
|
networkDevices, err := nc.ListNetworkInterfaces(ctx)
|
|
if err != nil {
|
|
return ssh.ProxmoxNode{}, fmt.Errorf("failed to list network devices of node %q: %w", nc.NodeName, err)
|
|
}
|
|
|
|
nodeAddress := ""
|
|
|
|
// try IPv4 address on the interface with IPv4 gateway
|
|
tflog.Debug(ctx, "Attempting to find interfaces with both a static IPV4 address and gateway.")
|
|
|
|
for _, d := range networkDevices {
|
|
if d.Gateway != nil && d.Address != nil {
|
|
nodeAddress = *d.Address
|
|
break
|
|
}
|
|
}
|
|
|
|
if nodeAddress == "" {
|
|
// fallback 1: try IPv6 address on the interface with IPv6 gateway
|
|
tflog.Debug(ctx, "Attempting to find interfaces with both a static IPV6 address and gateway.")
|
|
|
|
for _, d := range networkDevices {
|
|
if d.Gateway6 != nil && d.Address6 != nil {
|
|
nodeAddress = *d.Address6
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if nodeAddress == "" {
|
|
// fallback 2: use first interface with any IPv4 address
|
|
tflog.Debug(ctx, "Attempting to find interfaces with at least a static IPV4 address.")
|
|
|
|
for _, d := range networkDevices {
|
|
if d.Address != nil {
|
|
nodeAddress = *d.Address
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if nodeAddress == "" {
|
|
// fallback 3: do a good old DNS lookup
|
|
tflog.Debug(ctx, fmt.Sprintf("Attempting a DNS lookup of node %q.", nc.NodeName))
|
|
|
|
ips, err := net.LookupIP(nodeName)
|
|
if err != nil {
|
|
for _, ip := range ips {
|
|
if ipv4 := ip.To4(); ipv4 != nil {
|
|
nodeAddress = ipv4.String()
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
tflog.Debug(ctx, fmt.Sprintf("Failed to do a DNS lookup of the node: %s", err.Error()))
|
|
}
|
|
}
|
|
|
|
if nodeAddress == "" {
|
|
return ssh.ProxmoxNode{}, fmt.Errorf("failed to determine the IP address of node \"%s\"", nc.NodeName)
|
|
}
|
|
|
|
nodeAddressParts := strings.Split(nodeAddress, "/")
|
|
node := ssh.ProxmoxNode{Address: nodeAddressParts[0], Port: 22}
|
|
|
|
return node, nil
|
|
}
|
|
|
|
type apiResolverWithOverrides struct {
|
|
ar apiResolver
|
|
overrides map[string]ssh.ProxmoxNode
|
|
}
|
|
|
|
func (r *apiResolverWithOverrides) Resolve(ctx context.Context, nodeName string) (ssh.ProxmoxNode, error) {
|
|
if node, ok := r.overrides[nodeName]; ok {
|
|
return node, nil
|
|
}
|
|
|
|
return r.ar.Resolve(ctx, nodeName)
|
|
}
|