mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-06-30 10:33:46 +00:00
feat: SSH-Agent Support (#306)
* chore: add agent configuration bool * feat: add ssh-agent authentication mechanism for linux * chore: make sure ssh-agent auth is only executed on linux * chore: add ssh user override * chore: add ssh configuration block, check ssh config during VirtualEnvironmentClient creation * fix: handle case of empty ssh config block * chore: add ssh password auth fallback logic * fix: remove not needed runtime * fix linter errors & re-format * allow ssh agent on all POSIX systems * add `agent_socket` parameter * update docs and examples --------- Co-authored-by: zoop <zoop@zoop.li> Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
parent
37494a01b6
commit
9fa92423b5
@ -67,6 +67,35 @@ export PROXMOX_VE_PASSWORD="a-strong-password"
|
||||
terraform plan
|
||||
```
|
||||
|
||||
### SSH connection
|
||||
|
||||
The Proxmox provider can connect to a Proxmox node via SSH. This is used in
|
||||
the `proxmox_virtual_environment_vm` or `proxmox_virtual_environment_file`
|
||||
resource to execute commands on the node to perform actions that are not
|
||||
supported by Proxmox API. For example, to import VM disks, or to uploading
|
||||
certain type of resources, such as snippets.
|
||||
|
||||
The SSH connection configuration is provided via the optional `ssh` block in
|
||||
the `provider` block:
|
||||
|
||||
```terraform
|
||||
provider "proxmox" {
|
||||
endpoint = "https://10.0.0.2:8006/"
|
||||
username = "username@realm"
|
||||
password = "a-strong-password"
|
||||
insecure = true
|
||||
ssh {
|
||||
agent = true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If no `ssh` block is provided, the provider will attempt to connect to the
|
||||
target node using the credentials provided in the `username` and `password` fields.
|
||||
Note that the target node is identified by the `node` argument in the resource,
|
||||
and may be different from the Proxmox API endpoint. Please refer to the
|
||||
section below for all the available arguments in the `ssh` block.
|
||||
|
||||
## Argument Reference
|
||||
|
||||
In addition
|
||||
@ -87,3 +116,18 @@ Proxmox `provider` block:
|
||||
- `username` - (Required) The username and realm for the Proxmox Virtual
|
||||
Environment API (can also be sourced from `PROXMOX_VE_USERNAME`). For
|
||||
example, `root@pam`.
|
||||
- `ssh` - (Optional) The SSH connection configuration to a Proxmox node. This is
|
||||
a
|
||||
block, whose fields are documented below.
|
||||
- `username` - (Optional) The username to use for the SSH connection.
|
||||
Defaults to the username used for the Proxmox API connection. Can also be
|
||||
sourced from `PROXMOX_VE_SSH_USERNAME`.
|
||||
- `password` - (Optional) The password to use for the SSH connection.
|
||||
Defaults to the password used for the Proxmox API connection. Can also be
|
||||
sourced from `PROXMOX_VE_SSH_PASSWORD`.
|
||||
- `agent` - (Optional) Whether to use the SSH agent for the SSH
|
||||
authentication. Defaults to `false`. Can also be sourced
|
||||
from `PROXMOX_VE_SSH_AGENT`.
|
||||
- `agent_socket` - (Optional) The path to the SSH agent socket.
|
||||
Defaults to the value of the `SSH_AUTH_SOCK` environment variable. Can
|
||||
also be sourced from `PROXMOX_VE_SSH_AUTH_SOCK`.
|
||||
|
@ -3,4 +3,7 @@ provider "proxmox" {
|
||||
username = var.virtual_environment_username
|
||||
password = var.virtual_environment_password
|
||||
insecure = true
|
||||
ssh {
|
||||
agent = true
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* 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/. */
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package proxmox
|
||||
|
||||
@ -14,6 +16,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
@ -24,7 +27,7 @@ import (
|
||||
// NewVirtualEnvironmentClient creates and initializes a VirtualEnvironmentClient instance.
|
||||
func NewVirtualEnvironmentClient(
|
||||
endpoint, username, password, otp string,
|
||||
insecure bool,
|
||||
insecure bool, sshUsername string, sshPassword string, sshAgent bool, sshAgentSocket string,
|
||||
) (*VirtualEnvironmentClient, error) {
|
||||
u, err := url.ParseRequestURI(endpoint)
|
||||
if err != nil {
|
||||
@ -51,6 +54,12 @@ func NewVirtualEnvironmentClient(
|
||||
)
|
||||
}
|
||||
|
||||
if !strings.Contains(username, "@") {
|
||||
return nil, errors.New(
|
||||
"make sure the username for the Proxmox Virtual Environment API ends in '@pve or @pam'",
|
||||
)
|
||||
}
|
||||
|
||||
var pOTP *string
|
||||
|
||||
if otp != "" {
|
||||
@ -68,12 +77,31 @@ func NewVirtualEnvironmentClient(
|
||||
|
||||
httpClient := &http.Client{Transport: transport}
|
||||
|
||||
if sshUsername == "" {
|
||||
sshUsername = strings.Split(username, "@")[0]
|
||||
}
|
||||
|
||||
if sshPassword == "" {
|
||||
sshPassword = password
|
||||
}
|
||||
|
||||
if sshAgent && runtime.GOOS != "linux" && runtime.GOOS != "darwin" && runtime.GOOS != "freebsd" {
|
||||
return nil, errors.New(
|
||||
"the ssh agent flag is only supported on POSIX systems, please set it to 'false'" +
|
||||
" or remove it from your provider configuration",
|
||||
)
|
||||
}
|
||||
|
||||
return &VirtualEnvironmentClient{
|
||||
Endpoint: strings.TrimRight(u.String(), "/"),
|
||||
Insecure: insecure,
|
||||
OTP: pOTP,
|
||||
Password: password,
|
||||
Username: username,
|
||||
SSHUsername: sshUsername,
|
||||
SSHPassword: sshPassword,
|
||||
SSHAgent: sshAgent,
|
||||
SSHAgentSocket: sshAgentSocket,
|
||||
httpClient: httpClient,
|
||||
}, nil
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* 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/. */
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package proxmox
|
||||
|
||||
@ -24,6 +26,10 @@ type VirtualEnvironmentClient struct {
|
||||
OTP *string
|
||||
Password string
|
||||
Username string
|
||||
SSHUsername string
|
||||
SSHPassword string
|
||||
SSHAgent bool
|
||||
SSHAgentSocket string
|
||||
|
||||
authenticationData *VirtualEnvironmentAuthenticationResponseData
|
||||
httpClient *http.Client
|
||||
|
@ -1,6 +1,8 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
/*
|
||||
* 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/. */
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package proxmox
|
||||
|
||||
@ -21,6 +23,7 @@ import (
|
||||
"github.com/skeema/knownhosts"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/agent"
|
||||
)
|
||||
|
||||
// ExecuteNodeCommands executes commands on a given node.
|
||||
@ -193,8 +196,6 @@ func (c *VirtualEnvironmentClient) OpenNodeShell(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ur := strings.Split(c.Username, "@")
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to determine the home directory: %w", err)
|
||||
@ -246,8 +247,61 @@ func (c *VirtualEnvironmentClient) OpenNodeShell(
|
||||
})
|
||||
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
User: ur[0],
|
||||
Auth: []ssh.AuthMethod{ssh.Password(c.Password)},
|
||||
User: c.SSHUsername,
|
||||
Auth: []ssh.AuthMethod{ssh.Password(c.SSHPassword)},
|
||||
HostKeyCallback: cb,
|
||||
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
|
||||
}
|
||||
|
||||
tflog.Info(ctx, fmt.Sprintf("Agent is set to %t", c.SSHAgent))
|
||||
|
||||
if c.SSHAgent {
|
||||
sshClient, err := c.CreateSSHClientAgent(ctx, cb, kh, sshHost)
|
||||
if err != nil {
|
||||
tflog.Error(ctx, "Failed ssh connection through agent, "+
|
||||
"falling back to password authentication",
|
||||
map[string]interface{}{
|
||||
"error": err,
|
||||
})
|
||||
} else {
|
||||
return sshClient, nil
|
||||
}
|
||||
}
|
||||
|
||||
sshClient, err := ssh.Dial("tcp", sshHost, sshConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial %s: %w", sshHost, err)
|
||||
}
|
||||
|
||||
tflog.Debug(ctx, "SSH connection established", map[string]interface{}{
|
||||
"host": sshHost,
|
||||
"user": c.SSHUsername,
|
||||
})
|
||||
return sshClient, nil
|
||||
}
|
||||
|
||||
// CreateSSHClientAgent establishes an ssh connection through the agent authentication mechanism
|
||||
func (c *VirtualEnvironmentClient) CreateSSHClientAgent(
|
||||
ctx context.Context,
|
||||
cb ssh.HostKeyCallback,
|
||||
kh knownhosts.HostKeyCallback,
|
||||
sshHost string,
|
||||
) (*ssh.Client, error) {
|
||||
if c.SSHAgentSocket == "" {
|
||||
return nil, errors.New("failed connecting to SSH agent socket: the socket file is not defined, " +
|
||||
"authentication will fall back to password")
|
||||
}
|
||||
|
||||
conn, err := net.Dial("unix", c.SSHAgentSocket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed connecting to SSH auth socket '%s': %w", c.SSHAgentSocket, err)
|
||||
}
|
||||
|
||||
ag := agent.NewClient(conn)
|
||||
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
User: c.SSHUsername,
|
||||
Auth: []ssh.AuthMethod{ssh.PublicKeysCallback(ag.Signers), ssh.Password(c.SSHPassword)},
|
||||
HostKeyCallback: cb,
|
||||
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
|
||||
}
|
||||
@ -259,7 +313,7 @@ func (c *VirtualEnvironmentClient) OpenNodeShell(
|
||||
|
||||
tflog.Debug(ctx, "SSH connection established", map[string]interface{}{
|
||||
"host": sshHost,
|
||||
"user": ur[0],
|
||||
"user": c.SSHUsername,
|
||||
})
|
||||
return sshClient, nil
|
||||
}
|
||||
|
@ -18,13 +18,17 @@ import (
|
||||
|
||||
const (
|
||||
dvProviderOTP = ""
|
||||
|
||||
mkProviderVirtualEnvironment = "virtual_environment"
|
||||
mkProviderEndpoint = "endpoint"
|
||||
mkProviderInsecure = "insecure"
|
||||
mkProviderOTP = "otp"
|
||||
mkProviderPassword = "password"
|
||||
mkProviderUsername = "username"
|
||||
mkProviderSSH = "ssh"
|
||||
mkProviderSSHUsername = "username"
|
||||
mkProviderSSHPassword = "password"
|
||||
mkProviderSSHAgent = "agent"
|
||||
mkProviderSSHAgentSocket = "agent_socket"
|
||||
)
|
||||
|
||||
// ProxmoxVirtualEnvironment returns the object for this provider.
|
||||
@ -41,26 +45,46 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{},
|
||||
var err error
|
||||
var veClient *proxmox.VirtualEnvironmentClient
|
||||
|
||||
// Initialize the client for the Virtual Environment, if required.
|
||||
// Legacy configuration, wrapped in the deprecated `virtual_environment` block
|
||||
veConfigBlock := d.Get(mkProviderVirtualEnvironment).([]interface{})
|
||||
|
||||
if len(veConfigBlock) > 0 {
|
||||
veConfig := veConfigBlock[0].(map[string]interface{})
|
||||
veSSHConfig := veConfig[mkProviderSSH].(map[string]interface{})
|
||||
|
||||
veClient, err = proxmox.NewVirtualEnvironmentClient(
|
||||
veConfig[mkProviderEndpoint].(string),
|
||||
veConfig[mkProviderUsername].(string),
|
||||
veConfig[mkProviderSSH].(map[string]interface{})[mkProviderSSHUsername].(string),
|
||||
veConfig[mkProviderPassword].(string),
|
||||
veConfig[mkProviderOTP].(string),
|
||||
veConfig[mkProviderInsecure].(bool),
|
||||
veSSHConfig[mkProviderSSHUsername].(string),
|
||||
veSSHConfig[mkProviderSSHPassword].(string),
|
||||
veSSHConfig[mkProviderSSHAgent].(bool),
|
||||
veSSHConfig[mkProviderSSHAgentSocket].(string),
|
||||
)
|
||||
} else {
|
||||
sshconf := map[string]interface{}{
|
||||
mkProviderSSHUsername: "",
|
||||
mkProviderSSHPassword: "",
|
||||
mkProviderSSHAgent: false,
|
||||
mkProviderSSHAgentSocket: "",
|
||||
}
|
||||
|
||||
sshBlock, sshSet := d.GetOk(mkProviderSSH)
|
||||
if sshSet {
|
||||
sshconf = sshBlock.(*schema.Set).List()[0].(map[string]interface{})
|
||||
}
|
||||
|
||||
veClient, err = proxmox.NewVirtualEnvironmentClient(
|
||||
d.Get(mkProviderEndpoint).(string),
|
||||
d.Get(mkProviderUsername).(string),
|
||||
d.Get(mkProviderPassword).(string),
|
||||
d.Get(mkProviderOTP).(string),
|
||||
d.Get(mkProviderInsecure).(bool),
|
||||
sshconf[mkProviderSSHUsername].(string),
|
||||
sshconf[mkProviderSSHPassword].(string),
|
||||
sshconf[mkProviderSSHAgent].(bool),
|
||||
sshconf[mkProviderSSHAgentSocket].(string),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -98,5 +98,64 @@ func nestedProviderSchema() map[string]*schema.Schema {
|
||||
},
|
||||
ValidateFunc: validation.StringIsNotEmpty,
|
||||
},
|
||||
mkProviderSSH: {
|
||||
Type: schema.TypeSet,
|
||||
Optional: true,
|
||||
MaxItems: 1,
|
||||
Description: "The SSH connection configuration to a Proxmox node",
|
||||
Elem: &schema.Resource{
|
||||
Schema: map[string]*schema.Schema{
|
||||
mkProviderSSHUsername: {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Description: fmt.Sprintf("The username used for the SSH connection, "+
|
||||
"defaults to the user specified in '%s'", mkProviderUsername),
|
||||
DefaultFunc: schema.MultiEnvDefaultFunc(
|
||||
[]string{"PROXMOX_VE_SSH_USERNAME", "PM_VE_SSH_USERNAME"},
|
||||
nil,
|
||||
),
|
||||
ValidateFunc: validation.StringIsNotEmpty,
|
||||
},
|
||||
mkProviderSSHPassword: {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Description: fmt.Sprintf("The password used for the SSH connection, "+
|
||||
"defaults to the password specified in '%s'", mkProviderPassword),
|
||||
DefaultFunc: schema.MultiEnvDefaultFunc(
|
||||
[]string{"PROXMOX_VE_SSH_PASSWORD", "PM_VE_SSH_PASSWORD"},
|
||||
nil,
|
||||
),
|
||||
ValidateFunc: validation.StringIsNotEmpty,
|
||||
},
|
||||
mkProviderSSHAgent: {
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
Description: "Whether to use the SSH agent for the SSH authentication. Defaults to false",
|
||||
DefaultFunc: func() (interface{}, error) {
|
||||
for _, k := range []string{"PROXMOX_VE_SSH_AGENT", "PM_VE_SSH_AGENT"} {
|
||||
v := os.Getenv(k)
|
||||
|
||||
if v == "true" || v == "1" {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
},
|
||||
},
|
||||
mkProviderSSHAgentSocket: {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Description: "The path to the SSH agent socket. Defaults to the value of the `SSH_AUTH_SOCK` " +
|
||||
"environment variable",
|
||||
DefaultFunc: schema.MultiEnvDefaultFunc(
|
||||
[]string{"SSH_AUTH_SOCK", "PROXMOX_VE_SSH_AUTH_SOCK", "PM_VE_SSH_AUTH_SOCK"},
|
||||
nil,
|
||||
),
|
||||
ValidateFunc: validation.StringIsNotEmpty,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user