From 60fb679e9f31b3be3e05bb9b25a0deb0ab37c48c Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Mon, 5 Feb 2024 21:28:28 -0500 Subject: [PATCH] fix(file): use `sudo` for snippets upload (#1004) * fix(file): use `sudo` for snippets upload Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> * fix: linter Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> * fix: no more rm -rf Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --------- Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- .vscode/settings.json | 5 +++- docs/index.md | 8 +++---- proxmoxtf/resource/file.go | 49 +++++++++++++++++++++++++++++++++----- proxmoxtf/resource/sudo.go | 15 ++++++++++++ proxmoxtf/resource/vm.go | 8 +++---- 5 files changed, 70 insertions(+), 15 deletions(-) create mode 100644 proxmoxtf/resource/sudo.go diff --git a/.vscode/settings.json b/.vscode/settings.json index 08f76250..1b4a084d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,11 @@ { "git.alwaysSignOff": true, "cSpell.words": [ + "capi", "CLRF", "iothread", "keyctl", + "nolint", "proxmoxtf", "qcow", "rootfs", @@ -13,7 +15,8 @@ "virtio", "VLANID", "vmbr", - "VMID" + "VMID", + "vztmpl" ], "go.lintTool": "golangci-lint", "go.lintFlags": [ diff --git a/docs/index.md b/docs/index.md index 54f112ce..75e301c8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -160,12 +160,12 @@ You can configure the `sudo` privilege for the user via the command line on the sudo visudo ``` - Add the following line to the end of the file: + Add the following lines to the end of the file: ```sh terraform ALL=(root) NOPASSWD: /sbin/pvesm terraform ALL=(root) NOPASSWD: /sbin/qm - terraform ALL=(root) NOPASSWD: /usr/bin/echo tfpve + terraform ALL=(root) NOPASSWD: /usr/bin/mv /tmp/tfpve/* /var/lib/vz/* ``` Save the file and exit. @@ -179,10 +179,10 @@ You can configure the `sudo` privilege for the user via the command line on the - Test the SSH connection and password-less `sudo`: ```sh - ssh terraform@ sudo echo tfpve + ssh terraform@ sudo pvesm apiinfo ``` - You should be able to connect to the target node and see the output `tfpve` on the screen without being prompted for your password. + You should be able to connect to the target node and see the output containing `APIVER ` on the screen without being prompted for your password. ### Node IP address used for SSH connection diff --git a/proxmoxtf/resource/file.go b/proxmoxtf/resource/file.go index d82bcaa2..360dbc38 100644 --- a/proxmoxtf/resource/file.go +++ b/proxmoxtf/resource/file.go @@ -17,10 +17,12 @@ import ( "net/url" "os" "path/filepath" + "regexp" "sort" "strings" "time" + "github.com/google/uuid" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -538,7 +540,12 @@ func fileCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag switch *contentType { case "iso", "vztmpl": uploadTimeout := d.Get(mkResourceVirtualEnvironmentFileTimeoutUpload).(int) + _, err = capi.Node(nodeName).Storage(datastoreID).APIUpload(ctx, request, uploadTimeout, config.TempDir()) + if err != nil { + diags = append(diags, diag.FromErr(err)...) + return diags + } default: // For all other content types, we need to upload the file to the node's // datastore using SFTP. @@ -565,14 +572,44 @@ func fileCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag }...) } - remoteFileDir := *datastore.Path + // the temp directory is used to store the file on the node before moving it to the datastore + // will be created if it does not exist + tempFileDir := fmt.Sprintf("/tmp/tfpve/%s", uuid.NewString()) - err = capi.SSH().NodeUpload(ctx, nodeName, remoteFileDir, request) - } + err = capi.SSH().NodeUpload(ctx, nodeName, tempFileDir, request) + if err != nil { + diags = append(diags, diag.FromErr(err)...) + return diags + } - if err != nil { - diags = append(diags, diag.FromErr(err)...) - return diags + // handle the case where the file is uploaded to a subdirectory of the datastore + srcDir := tempFileDir + dstDir := *datastore.Path + + if request.ContentType != "" { + srcDir = tempFileDir + "/" + request.ContentType + dstDir = *datastore.Path + "/" + request.ContentType + } + + _, err := capi.SSH().ExecuteNodeCommands(ctx, nodeName, []string{ + // the `mv` command should be scoped to the specific directories in sudoers! + fmt.Sprintf(`%s; try_sudo "mv %s/%s %s/%s" && rm %s/%s && rmdir -p %s`, + trySudo, + srcDir, *fileName, + dstDir, *fileName, + srcDir, *fileName, + srcDir, + ), + }) + if err != nil { + if matches, e := regexp.MatchString(`cannot move .* Permission denied`, err.Error()); e == nil && matches { + return diag.FromErr(newErrSSHUserNoPermission(capi.SSH().Username())) + } + + diags = append(diags, diag.Errorf("error moving file: %s", err.Error())...) + + return diags + } } volID, di := fileGetVolumeID(d) diff --git a/proxmoxtf/resource/sudo.go b/proxmoxtf/resource/sudo.go new file mode 100644 index 00000000..d9598a14 --- /dev/null +++ b/proxmoxtf/resource/sudo.go @@ -0,0 +1,15 @@ +package resource + +import ( + "fmt" +) + +const ( + trySudo = `try_sudo(){ if [ $(sudo -n pvesm apiinfo 2>&1 | grep "APIVER" | wc -l) -gt 0 ]; then sudo $1; else $1; fi }` +) + +func newErrSSHUserNoPermission(username string) error { + return fmt.Errorf("the SSH user '%s' does not have required permissions. "+ + "Make sure 'sudo' is installed and the user is configured in sudoers file. "+ + "Refer to the documentation for more details", username) +} diff --git a/proxmoxtf/resource/vm.go b/proxmoxtf/resource/vm.go index fd57a19c..b2df642c 100644 --- a/proxmoxtf/resource/vm.go +++ b/proxmoxtf/resource/vm.go @@ -11,6 +11,7 @@ import ( "encoding/base64" "errors" "fmt" + "regexp" "sort" "strconv" "strings" @@ -2974,7 +2975,7 @@ func vmCreateCustomDisks(ctx context.Context, d *schema.ResourceData, m interfac commands = append( commands, `set -e`, - `try_sudo(){ if [ $(sudo -n echo tfpve 2>&1 | grep "tfpve" | wc -l) -gt 0 ]; then sudo $1; else $1; fi }`, + trySudo, fmt.Sprintf(`file_id="%s"`, fileID), fmt.Sprintf(`file_format="%s"`, fileFormat), fmt.Sprintf(`datastore_id_target="%s"`, datastoreID), @@ -3007,9 +3008,8 @@ func vmCreateCustomDisks(ctx context.Context, d *schema.ResourceData, m interfac out, err := api.SSH().ExecuteNodeCommands(ctx, nodeName, commands) if err != nil { - if strings.Contains(err.Error(), "pvesm: not found") { - return diag.Errorf("The configured SSH user '%s' does not have the required permissions to import disks. "+ - "Make sure `sudo` is installated and the user is a member of sudoers.", api.SSH().Username()) + if matches, e := regexp.Match(`pvesm: .* not found`, out); e == nil && matches { + return diag.FromErr(newErrSSHUserNoPermission(api.SSH().Username())) } return diag.FromErr(err)