Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 83 additions & 19 deletions internal/volumes/cryptsetup.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"errors"
"fmt"
"log/slog"
"os"
"os/exec"
"strings"
"syscall"
)

const cryptsetupExecuable = "cryptsetup"
const cryptsetupExecutable = "cryptsetup"

type CryptSetup struct {
logger *slog.Logger
Expand All @@ -19,62 +21,89 @@ func NewCryptSetup(logger *slog.Logger) *CryptSetup {
return &CryptSetup{logger: logger}
}

func (cs *CryptSetup) IsActive(ctx context.Context, luksDeviceName string) (bool, error) {
output, code, err := command(ctx, cryptsetupExecuable, "status", luksDeviceName)
func (cs *CryptSetup) Status(ctx context.Context, luksDeviceName string) (LUKSDeviceStatus, error) {
output, code, err := cryptsetup(ctx, "status", luksDeviceName)
if err != nil {
if code == 4 {
return false, nil
return LUKSDeviceStatus{Active: false}, nil
}
return false, fmt.Errorf("unable to check LUKS device %s activity: %s", luksDeviceName, output)
return LUKSDeviceStatus{Active: false}, fmt.Errorf("unable to check LUKS device %s status: %w", luksDeviceName, err)
}
return true, nil

return NewLUKSDeviceStatus(output), nil
}

func (cs *CryptSetup) Format(ctx context.Context, devicePath string, passphrase string) error {
cs.logger.Info(
"formatting LUKS device",
"devicePath", devicePath,
)
output, _, err := commandWithStdin(ctx, passphrase, cryptsetupExecuable, "luksFormat", "--type", "luks1", devicePath)
output, _, err := cryptsetupWithStdin(ctx, passphrase, "luksFormat", "--type", "luks1", devicePath)
if err != nil {
return fmt.Errorf("unable to format device %s with LUKS: %s", devicePath, output)
}
return nil
}

func (cs *CryptSetup) Open(ctx context.Context, devicePath string, luksDeviceName string, passphrase string) error {
active, err := cs.IsActive(ctx, luksDeviceName)
status, err := cs.Status(ctx, luksDeviceName)
if err != nil {
return err
}
if active {
return nil
if status.Active {
if sameDevice(status.Device, devicePath) {
cs.logger.Debug(
"LUKS device already active",
"devicePath", devicePath,
"luksDeviceName", luksDeviceName,
)
return nil
}

// A volume detached without being unpublished leaves behind a mapper whose backing
// device is gone. Reusing it would format and mount a dead device.
cs.logger.Warn(
"LUKS device is not backed by the expected device, closing it",
"devicePath", devicePath,
"luksDeviceName", luksDeviceName,
"backingDevice", status.Device,
)
if err := cs.Close(ctx, luksDeviceName); err != nil {
return fmt.Errorf(
"LUKS device %s is backed by %s instead of %s: %w",
luksDeviceName,
status.Device,
devicePath,
err,
)
}
}

cs.logger.Info(
"opening LUKS device",
"devicePath", devicePath,
"luksDeviceName", luksDeviceName,
)
output, _, err := commandWithStdin(ctx, passphrase, cryptsetupExecuable, "luksOpen", "--allow-discards", devicePath, luksDeviceName)
output, _, err := cryptsetupWithStdin(ctx, passphrase, "luksOpen", "--allow-discards", devicePath, luksDeviceName)
if err != nil {
return fmt.Errorf("unable to open LUKS device %s: %s", devicePath, output)
}
return nil
}

func (cs *CryptSetup) Close(ctx context.Context, luksDeviceName string) error {
active, err := cs.IsActive(ctx, luksDeviceName)
status, err := cs.Status(ctx, luksDeviceName)
if err != nil {
return err
}
if !active {
if !status.Active {
return nil
}
cs.logger.Info(
"closing LUKS device",
"luksDeviceName", luksDeviceName,
)
output, _, err := command(ctx, cryptsetupExecuable, "luksClose", luksDeviceName)
output, _, err := cryptsetup(ctx, "luksClose", luksDeviceName)
if err != nil {
return fmt.Errorf("unable to close LUKS device %s: %s", luksDeviceName, output)
}
Expand All @@ -86,7 +115,7 @@ func (cs *CryptSetup) Resize(ctx context.Context, luksDeviceName string) error {
"resizing LUKS device",
"luksDeviceName", luksDeviceName,
)
output, _, err := command(ctx, cryptsetupExecuable, "resize", luksDeviceName)
output, _, err := cryptsetup(ctx, "resize", luksDeviceName)
if err != nil {
return fmt.Errorf("unable to resize LUKS device %s: %s", luksDeviceName, output)
}
Expand All @@ -102,12 +131,12 @@ func GenerateLUKSDevicePath(luksDeviceName string) string {
return "/dev/mapper/" + luksDeviceName
}

func command(ctx context.Context, name string, args ...string) (string, int, error) {
return commandWithStdin(ctx, "", name, args...)
func cryptsetup(ctx context.Context, args ...string) (string, int, error) {
return cryptsetupWithStdin(ctx, "", args...)
}

func commandWithStdin(ctx context.Context, stdin string, name string, args ...string) (string, int, error) {
cmd := exec.CommandContext(ctx, name, args...)
func cryptsetupWithStdin(ctx context.Context, stdin string, args ...string) (string, int, error) {
cmd := exec.CommandContext(ctx, cryptsetupExecutable, args...)
if stdin != "" {
cmd.Stdin = strings.NewReader(stdin)
}
Expand All @@ -122,3 +151,38 @@ func commandWithStdin(ctx context.Context, stdin string, name string, args ...st
}
return output, 0, nil
}

type LUKSDeviceStatus struct {
Active bool
Device string
}

func NewLUKSDeviceStatus(output string) LUKSDeviceStatus {
// An inactive device is reported through the exit code, not the output.
status := LUKSDeviceStatus{Active: true}
for line := range strings.SplitSeq(output, "\n") {
key, value, ok := strings.Cut(strings.TrimSpace(line), ":")
if ok && key == "device" {
status.Device = strings.TrimSpace(value)
}
}
return status
}

// sameDevice reports whether a and b refer to the same device node.
//
// os.ModeDevice is set for block and character device files only. Both paths
// must be device nodes because Rdev (the major:minor device number) is only
// meaningful for them: for regular files and directories it is 0, so two
// unrelated non-device paths would otherwise compare as equal.
func sameDevice(a string, b string) bool {
infoA, err := os.Stat(a)
if err != nil || infoA.Mode()&os.ModeDevice == 0 {
return false
}
infoB, err := os.Stat(b)
if err != nil || infoB.Mode()&os.ModeDevice == 0 {
return false
}
return infoA.Sys().(*syscall.Stat_t).Rdev == infoB.Sys().(*syscall.Stat_t).Rdev
}
112 changes: 112 additions & 0 deletions internal/volumes/cryptsetup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package volumes

import (
"os"
"path/filepath"
"testing"
)

func TestNewLUKSDeviceStatus(t *testing.T) {
testCases := []struct {
Name string
Output string
Want LUKSDeviceStatus
}{
{
Name: "healthy device",
Output: `/dev/mapper/pv-1 is active and is in use.
type: LUKS1
cipher: aes-xts-plain64
keysize: 512 bits
device: /dev/sdb
sector size: 512
offset: 4096 sectors
size: 2093056 sectors
mode: read/write`,
Want: LUKSDeviceStatus{Active: true, Device: "/dev/sdb"},
},
{
Name: "backing device disappeared",
Output: `/dev/mapper/pv-1 is active.
type: n/a
cipher: aes-xts-plain64
keysize: 512 bits
device: (null)
mode: read/write`,
Want: LUKSDeviceStatus{Active: true, Device: "(null)"},
},
{
Name: "device line missing",
Output: `/dev/mapper/pv-1 is active.
type: LUKS1
cipher: aes-xts-plain64`,
Want: LUKSDeviceStatus{Active: true},
},
{
Name: "empty output",
Output: "",
Want: LUKSDeviceStatus{Active: true},
},
}

for _, testCase := range testCases {
t.Run(testCase.Name, func(t *testing.T) {
if got := NewLUKSDeviceStatus(testCase.Output); got != testCase.Want {
t.Errorf("NewLUKSDeviceStatus() = %+v, want %+v", got, testCase.Want)
}
})
}
}

func TestSameDevice(t *testing.T) {
regularFile := filepath.Join(t.TempDir(), "file")
if err := os.WriteFile(regularFile, nil, 0o600); err != nil {
t.Fatal(err)
}

testCases := []struct {
Name string
A string
B string
Want bool
}{
{
Name: "same device",
A: "/dev/null",
B: "/dev/null",
Want: true,
},
{
Name: "different devices",
A: "/dev/null",
B: "/dev/zero",
Want: false,
},
{
Name: "backing device disappeared",
A: "(null)",
B: "/dev/null",
Want: false,
},
{
Name: "backing device missing",
A: "",
B: "/dev/null",
Want: false,
},
{
Name: "regular files",
A: regularFile,
B: regularFile,
Want: false,
},
}

for _, testCase := range testCases {
t.Run(testCase.Name, func(t *testing.T) {
if got := sameDevice(testCase.A, testCase.B); got != testCase.Want {
t.Errorf("sameDevice(%q, %q) = %v, want %v", testCase.A, testCase.B, got, testCase.Want)
}
})
}
}
4 changes: 2 additions & 2 deletions internal/volumes/resize.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ func (l *LinuxResizeService) Resize(ctx context.Context, volumePath string, volu
)

luksDeviceName := GenerateLUKSDeviceName(devicePath)
active, err := l.cryptSetup.IsActive(ctx, luksDeviceName)
status, err := l.cryptSetup.Status(ctx, luksDeviceName)
if err != nil {
return err
}
if active {
if status.Active {
luksDevicePath := GenerateLUKSDevicePath(luksDeviceName)
if err := l.cryptSetup.Resize(ctx, luksDeviceName); err != nil {
return err
Expand Down
Loading
Loading