diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5d3a326..38aa2d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,11 @@ jobs: run: | go mod download + - name: Install Deps + run: | + sudo apt-get update --quiet + sudo apt-get install --quiet --assume-yes --no-install-recommends lvm2 thin-provisioning-tools + - name: build run: | make build diff --git a/linux/lvm.go b/linux/lvm.go index 7e23b83..072cb2c 100644 --- a/linux/lvm.go +++ b/linux/lvm.go @@ -9,6 +9,9 @@ import ( "github.com/anuvu/disko" ) +const pvMetaDataSize = 128 * disko.Mebibyte +const thinPoolMetaDataSize = 1024 * disko.Mebibyte + // VolumeManager returns the linux implementation of disko.VolumeManager interface. func VolumeManager() disko.VolumeManager { return &linuxLVM{} @@ -168,7 +171,8 @@ func (ls *linuxLVM) CreatePV(name string) (disko.PV, error) { return nilPV, err } - err = runCommandSettled("lvm", "pvcreate", "--zero=y", path) + err = runCommandSettled("lvm", "pvcreate", "--zero=y", + fmt.Sprintf("--metadatasize=%dB", pvMetaDataSize), path) if err != nil { return nilPV, err @@ -201,9 +205,8 @@ func (ls *linuxLVM) HasPV(name string) bool { } func (ls *linuxLVM) CreateVG(name string, pvs ...disko.PV) (disko.VG, error) { - const mdSize = 128 * disko.Mebibyte cmd := []string{"lvm", "vgcreate", - fmt.Sprintf("--metadatasize=%dB", mdSize), + fmt.Sprintf("--metadatasize=%dB", pvMetaDataSize), "--zero=y", name} for _, p := range pvs { @@ -269,6 +272,25 @@ func (ls *linuxLVM) CryptClose(vgName string, lvName string, return runCommand("cryptsetup", "close", decryptedName) } +func createLVCmd(args ...string) error { + return runCommandSettled( + append([]string{"lvm", "lvcreate", "--ignoremonitoring", "--yes", "--activate=y", + "--setactivationskip=n"}, args...)...) +} + +func createThinPool(name string, vgName string, size uint64, mdSize uint64) error { + // thinpool takes up size + 2*mdSize + // https://www.redhat.com/archives/linux-lvm/2020-October/thread.html#00016 + args := []string{} + // if mdSize is zero, let lvcreate choose the size. That is documented as: + // (Pool_LV_size / Pool_LV_chunk_size * 64) + if mdSize != 0 { + args = append(args, fmt.Sprintf("--poolmetadatasize=%dB", mdSize)) + } + + return createLVCmd(append(args, fmt.Sprintf("--size=%dB", size), "--thinpool="+name, vgName)...) +} + func (ls *linuxLVM) CreateLV(vgName string, name string, size uint64, lvType disko.LVType) (disko.LV, error) { nilLV := disko.LV{} @@ -277,22 +299,35 @@ func (ls *linuxLVM) CreateLV(vgName string, name string, size uint64, return nilLV, err } - if lvType == disko.THIN { - // thin lv creation would require creating a pool - return nilLV, fmt.Errorf("not supported. Thin LV create not implemented") - } + nameFlag := "--name=" + name + sizeB := fmt.Sprintf("%dB", size) + vglv := vgLv(vgName, name) - err := runCommandSettled( - "lvm", "lvcreate", "--ignoremonitoring", "--yes", "--activate=y", - "--zero=y", - "--setactivationskip=n", fmt.Sprintf("--size=%dB", size), - fmt.Sprintf("--name=%s", name), vgName) + switch lvType { + case disko.THIN: + // When creating THIN LV, the VG must be / + if !strings.Contains(vgName, "/") { + return nilLV, + fmt.Errorf("%s: vgName input for THIN LV name in format /thinDataName", vgName) + } - if err != nil { - return nilLV, err + vglv = vgLv(strings.Split(vgName, "/")[0], name) + + if err := createLVCmd("--virtualsize="+sizeB, nameFlag, vgName); err != nil { + return nilLV, err + } + case disko.THICK: + if err := createLVCmd("--size="+sizeB, nameFlag, vgName); err != nil { + return nilLV, err + } + case disko.THINPOOL: + // When creating a THINPOOL, the name is the thin pool name. + if err := createThinPool(name, vgName, size, thinPoolMetaDataSize); err != nil { + return nilLV, err + } } - lvs, err := ls.scanLVs(func(d disko.LV) bool { return true }, vgLv(vgName, name)) + lvs, err := ls.scanLVs(func(d disko.LV) bool { return true }, vglv) if err != nil { return nilLV, err diff --git a/linux/root_test.go b/linux/root_test.go index 75fc8ba..a35168c 100644 --- a/linux/root_test.go +++ b/linux/root_test.go @@ -234,3 +234,98 @@ func TestRootLVMExtend(t *testing.T) { foundLv = vgs[vgname].Volumes[lvname] ast.Equalf(size2, foundLv.Size, "extended volume size incorrect") } + +func runShow(args ...string) { + out, err, rc := runCommandWithOutputErrorRc(args...) + fmt.Print(cmdString(args, out, err, rc)) +} + +func TestRootLVMCreate(t *testing.T) { + iSkipOrFail(t, isRoot, canUseLoop, canUseLVM) + + ast := assert.New(t) + + var cl = cleanList{} + defer cl.Cleanup(t) + + var pv disko.PV + var vg disko.VG + var lv disko.LV + var c cleaner + var tmpFile string + + lvthick := "diskot-thick" + randStr(8) + lvthinpool := "diskot-pool" + randStr(8) + lvthin := "diskot-thin" + randStr(8) + vgname := "diskot-vg" + randStr(8) + + c, tmpFile = getTempFile(4 * GiB) + cl.Add(c) + + lCleanup, disk, err := singlePartDisk(tmpFile) + cl.AddF(lCleanup, "singlePartdisk") + + if err != nil { + t.Fatalf("Failed to create a single part disk: %s", err) + } + + lvm := linux.VolumeManager() + + pv, err = lvm.CreatePV(disk.Path + "p1") + if err != nil { + t.Fatalf("Failed to create pv on %s: %s\n", disk.Path, err) + } + + cl.AddF(func() error { return lvm.DeletePV(pv) }, "remove pv") + + vg, err = lvm.CreateVG(vgname, pv) + + if err != nil { + t.Fatalf("Failed to create %s with %s: %s", vgname, pv.Path, err) + } + + cl.AddF(func() error { return lvm.RemoveVG(vgname) }, "remove VG") + + ast.Equal(vgname, vg.Name) + + thickSize := uint64(12 * MiB) + + lv, err = lvm.CreateLV(vgname, lvthick, thickSize, disko.THICK) + if err != nil { + t.Fatalf("Failed to create lv %s/%s: %s", vgname, lvthick, err) + } + + cl.AddF(func() error { return lvm.RemoveLV(vgname, lvthick) }, "remove LV") + + ast.Equal(lvthick, lv.Name) + ast.Equal(thickSize, lv.Size) + + thinPoolSize, thinSize := uint64(500*MiB), uint64(200*MiB) + + // create a THINPOOL volume + lv, err = lvm.CreateLV(vgname, lvthinpool, thinPoolSize, disko.THINPOOL) + if err != nil { + t.Fatalf("Failed to create lv %s/%s: %s", vgname, lvthick, err) + } + + cl.AddF(func() error { return lvm.RemoveLV(vgname, lvthinpool) }, "remove thin pool LV") + + ast.Equal(lvthinpool, lv.Name) + ast.Equal(thinPoolSize, lv.Size) + + lv, err = lvm.CreateLV(vgname+"/"+lvthinpool, lvthin, thinSize, disko.THIN) + if err != nil { + runShow("lvm", "lvdisplay", "--unit=m", vgname) + t.Fatalf("Failed to create THIN lv %s on %s/%s: %s", lvthin, vgname, lvthinpool, err) + } + + ast.Equal(lvthin, lv.Name) + ast.Equal(thinSize, lv.Size) + + vgs, errScan := lvm.ScanVGs(func(v disko.VG) bool { return v.Name == vgname }) + if errScan != nil { + t.Fatalf("Failed to scan VGs: %s\n", err) + } + + ast.Equal(len(vgs), 1) +}