├── doc.go ├── disko.go ├── mpi3mr ├── linux.go ├── mpi3mr.go └── storcli2_test.go ├── smartpqi ├── linux.go └── smartpqi.go ├── megaraid ├── linux.go └── megaraid.go ├── .gitignore ├── linux ├── raidcontroller.go ├── virt_test.go ├── lvm_test.go ├── virt.go ├── lvmdump_test.go ├── lvmdump.go ├── util_test.go ├── system_test.go ├── root_helpers_test.go ├── system.go ├── util.go └── lvm.go ├── partid ├── partid_test.go └── partid.go ├── .github └── workflows │ ├── lint.yml │ └── build.yml ├── README.md ├── guid.go ├── go.mod ├── util_test.go ├── demo ├── lvm.go ├── main.go ├── megaraid.go ├── smartpqi.go ├── mpi3mr.go ├── disk.go └── misc.go ├── lvm_test.go ├── guid_test.go ├── util.go ├── system.go ├── Makefile ├── mockos ├── testdata │ └── model_sys.json ├── system.go ├── system_test.go ├── lvm_test.go └── lvm.go ├── go.sum ├── lvm.go ├── disk_test.go ├── LICENSE └── disk.go /doc.go: -------------------------------------------------------------------------------- 1 | // Package disko - disk operations 2 | package disko 3 | -------------------------------------------------------------------------------- /disko.go: -------------------------------------------------------------------------------- 1 | package disko 2 | 3 | // Mebibyte defines 1MiB 4 | const Mebibyte uint64 = 1024 * 1024 5 | -------------------------------------------------------------------------------- /mpi3mr/linux.go: -------------------------------------------------------------------------------- 1 | package mpi3mr 2 | 3 | const SysfsPCIDriversPath = "/sys/bus/pci/drivers/mpi3mr" 4 | -------------------------------------------------------------------------------- /smartpqi/linux.go: -------------------------------------------------------------------------------- 1 | package smartpqi 2 | 3 | const SysfsPCIDriversPath = "/sys/bus/pci/drivers/smartpqi" 4 | -------------------------------------------------------------------------------- /megaraid/linux.go: -------------------------------------------------------------------------------- 1 | package megaraid 2 | 3 | const SysfsPCIDriversPath = "/sys/bus/pci/drivers/megaraid_sas" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Intellij 9 | .idea/ 10 | *.iml 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | -------------------------------------------------------------------------------- /linux/raidcontroller.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import "machinerun.io/disko" 4 | 5 | type RAIDControllerType string 6 | 7 | const ( 8 | MegaRAIDControllerType RAIDControllerType = "megaraid" 9 | SmartPqiControllerType RAIDControllerType = "smartpqi" 10 | MPI3MRControllerType RAIDControllerType = "mpi3mr" 11 | ) 12 | 13 | type RAIDController interface { 14 | // Type() RAIDControllerType 15 | GetDiskType(string) (disko.DiskType, error) 16 | IsSysPathRAID(string) bool 17 | DriverSysfsPath() string 18 | } 19 | -------------------------------------------------------------------------------- /partid/partid_test.go: -------------------------------------------------------------------------------- 1 | package partid_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "machinerun.io/disko/partid" 7 | ) 8 | 9 | func TestPartID(t *testing.T) { 10 | // Not a very good test, but something. 11 | for id, text := range map[[16]byte]string{ 12 | partid.LinuxFS: "Linux-FS", 13 | partid.LinuxLVM: "LVM", 14 | partid.LinuxRAID: "RAID", 15 | } { 16 | if partid.Text[id] != text { 17 | t.Errorf("Unexpected text. found %s expected %s", 18 | partid.Text[id], text) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [push, pull_request] 3 | jobs: 4 | lint: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Install Go 8 | uses: actions/setup-go@v4 9 | with: 10 | go-version: '1.22.x' 11 | cache: false 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | - name: golangci-lint 15 | uses: golangci/golangci-lint-action@v3 16 | with: 17 | version: v1.63.4 18 | - name: gofmt 19 | run: | 20 | make gofmt 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :dvd: disko 2 | 3 | Disk Operations API in Go 4 | 5 | [![Actions Status](https://machinerun.io/disko/workflows/Build/badge.svg)](https://machinerun.io/disko/actions) 6 | [![Actions Status](https://machinerun.io/disko/workflows/Lint/badge.svg)](https://machinerun.io/disko/actions) 7 | [![codecov](https://codecov.io/gh/project-machine/disko/graph/badge.svg?token=OWZk2EXt3x)](https://codecov.io/gh/project-machine/disko) 8 | [![Go Report Card](https://goreportcard.com/badge/machinerun.io/disko)](https://goreportcard.com/report/machinerun.io/disko) -------------------------------------------------------------------------------- /guid.go: -------------------------------------------------------------------------------- 1 | package disko 2 | 3 | import ( 4 | "github.com/rekby/gpt" 5 | uuid "github.com/satori/go.uuid" 6 | ) 7 | 8 | // GUID - a 16 byte Globally Unique ID 9 | type GUID [16]byte 10 | 11 | // GenGUID - generate a random uuid and return it 12 | func GenGUID() GUID { 13 | return GUID(uuid.NewV4()) 14 | } 15 | 16 | func (g GUID) String() string { 17 | return GUIDToString(g) 18 | } 19 | 20 | // StringToGUID - convert a string to a GUID 21 | func StringToGUID(sguid string) (GUID, error) { 22 | return gpt.StringToGuid(sguid) 23 | } 24 | 25 | // GUIDToString - turn a Guid into a string. 26 | func GUIDToString(bguid GUID) string { 27 | return gpt.Guid(bguid).String() 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module machinerun.io/disko 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/google/go-cmp v0.6.0 7 | github.com/patrickmn/go-cache v2.1.0+incompatible 8 | github.com/pkg/errors v0.9.1 9 | github.com/rekby/gpt v0.0.0-20200614112001-7da10aec5566 10 | github.com/rekby/mbr v0.0.0-20190325193910-2b19b9cdeebc 11 | github.com/satori/go.uuid v1.2.0 12 | github.com/smartystreets/goconvey v1.8.0 13 | github.com/stretchr/testify v1.8.2 14 | github.com/urfave/cli/v2 v2.25.3 15 | golang.org/x/sys v0.8.0 16 | ) 17 | 18 | require ( 19 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/gopherjs/gopherjs v1.17.2 // indirect 22 | github.com/jtolds/gls v4.20.0+incompatible // indirect 23 | github.com/pmezard/go-difflib v1.0.0 // indirect 24 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 25 | github.com/smartystreets/assertions v1.13.1 // indirect 26 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 27 | gopkg.in/yaml.v3 v3.0.1 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package disko 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFindGaps(t *testing.T) { 10 | assert := assert.New(t) 11 | type data struct { 12 | expected uRanges 13 | ranges uRanges 14 | min, max uint64 15 | } 16 | 17 | for _, d := range []data{ 18 | {uRanges{{0, 100}}, uRanges{}, 0, 100}, 19 | {uRanges{{0, 49}, {60, 100}}, uRanges{{50, 59}}, 0, 100}, 20 | {uRanges{{51, 100}}, uRanges{{0, 50}}, 0, 100}, 21 | {uRanges{{0, 10}}, uRanges{{11, 100}}, 0, 100}, 22 | {uRanges{{0, 10}, {50, 59}, {91, 100}}, 23 | uRanges{{11, 49}, {60, 90}}, 0, 100}, 24 | {uRanges{}, uRanges{{0, 10}, {11, 100}}, 0, 100}, 25 | {uRanges{}, uRanges{{0, 150}, {50, 100}}, 100, 100}, 26 | {uRanges{{0, 9}, {41, 49}, {101, 110}}, 27 | uRanges{{10, 40}, {50, 100}}, 0, 110}, 28 | {uRanges{{10, 100}}, uRanges{{110, 200}}, 10, 100}, 29 | {uRanges{{0, 9}, {51, 89}}, uRanges{{90, 100}, {10, 50}}, 0, 100}, 30 | {uRanges{{2, 3}, {26, 52}, {62, 98}}, 31 | uRanges{{0, 1}, {99, 100}, {53, 61}, {4, 25}}, 0, 100}, 32 | } { 33 | assert.Equal(d.expected, findRangeGaps(d.ranges, d.min, d.max)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | name: Test 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Set up Go 9 | uses: actions/setup-go@v4 10 | with: 11 | go-version: '1.22.x' 12 | id: go 13 | 14 | - name: Check out code into the Go module directory 15 | uses: actions/checkout@v1 16 | 17 | - name: mod-download 18 | run: | 19 | go mod download 20 | 21 | - name: Install Deps 22 | run: | 23 | sudo apt-get update --quiet 24 | sudo apt-get install --quiet --assume-yes --no-install-recommends lvm2 thin-provisioning-tools 25 | 26 | - name: gofmt 27 | run: | 28 | make gofmt 29 | 30 | - name: build 31 | run: | 32 | make build 33 | 34 | - name: Unit Test 35 | run: | 36 | make test 37 | 38 | - name: System Test 39 | run: | 40 | make test-all 41 | 42 | - name: Upload Coverage report to CodeCov 43 | if: success() 44 | uses: codecov/codecov-action@v1.0.0 45 | with: 46 | token: 78b19480-8cfa-4f32-8448-b972a29a8f46 47 | file: ./coverage-all.txt 48 | -------------------------------------------------------------------------------- /demo/lvm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/urfave/cli/v2" 8 | "machinerun.io/disko" 9 | "machinerun.io/disko/linux" 10 | ) 11 | 12 | //nolint:gochecknoglobals 13 | var lvmCommands = cli.Command{ 14 | Name: "lvm", 15 | Usage: "lvm commands", 16 | Subcommands: []*cli.Command{ 17 | { 18 | Name: "dump-vgs", 19 | Usage: "Scan system and dump disko VGs. Optionally give a vg name.", 20 | Action: lvmDumpVGs, 21 | }, 22 | }, 23 | } 24 | 25 | func lvmDumpVGs(c *cli.Context) error { 26 | var filter disko.VGFilter 27 | 28 | if c.Args().Len() == 0 { 29 | filter = func(v disko.VG) bool { return true } 30 | } else if c.Args().Len() == 1 { 31 | filter = func(v disko.VG) bool { return v.Name == c.Args().First() } 32 | } else { 33 | return fmt.Errorf("too many args. Really just want 1. Got %d", c.Args().Len()) 34 | } 35 | 36 | vmgr := linux.VolumeManager() 37 | 38 | vgset, err := vmgr.ScanVGs(filter) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | jbytes, err := json.MarshalIndent(&vgset, "", " ") 44 | if err != nil { 45 | return err 46 | } 47 | 48 | fmt.Println(string(jbytes)) 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | var version string 13 | 14 | func printTextTable(data [][]string) { 15 | var lengths = make([]int, len(data[0])) 16 | 17 | for _, line := range data { 18 | for i, field := range line { 19 | if len(field) > lengths[i] { 20 | lengths[i] = len(field) 21 | } 22 | } 23 | } 24 | 25 | fmts := make([]string, len(lengths)) 26 | 27 | for i, l := range lengths { 28 | fmts[i] = fmt.Sprintf("%%-%ds", l) 29 | } 30 | 31 | pfmt := strings.Join(fmts, " | ") + " |\n" 32 | 33 | for _, line := range data { 34 | s := make([]interface{}, len(line)) 35 | for i, v := range line { 36 | s[i] = v 37 | } 38 | 39 | fmt.Printf(pfmt, s...) 40 | } 41 | } 42 | 43 | func main() { 44 | app := &cli.App{ 45 | Name: "disko-demo", 46 | Version: version, 47 | Usage: "Play around or test disko", 48 | Commands: []*cli.Command{ 49 | &diskCommands, 50 | &megaraidCommands, 51 | &smartpqiCommands, 52 | &mpi3mrCommands, 53 | &lvmCommands, 54 | &miscCommands, 55 | }, 56 | } 57 | 58 | err := app.Run(os.Args) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lvm_test.go: -------------------------------------------------------------------------------- 1 | package disko_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "machinerun.io/disko" 10 | ) 11 | 12 | var valid = map[string]disko.LVType{ 13 | "THICK": disko.THICK, 14 | "THIN": disko.THIN, 15 | "THINPOOL": disko.THINPOOL, 16 | } 17 | 18 | func TestLVTypeString(t *testing.T) { 19 | for asStr, ltype := range valid { 20 | found := ltype.String() 21 | if found != asStr { 22 | t.Errorf("disko.LVType(%d).String() found %s, expected %s", 23 | ltype, found, asStr) 24 | } 25 | } 26 | } 27 | 28 | func TestLVTypeJsonSerialize(t *testing.T) { 29 | for asStr, ltype := range valid { 30 | ltype := ltype 31 | 32 | jbytes, err := json.Marshal(<ype) 33 | if err != nil { 34 | t.Errorf("Failed to marshal %#v: %s", ltype, err) 35 | continue 36 | } 37 | 38 | jstr := string(jbytes) 39 | if !strings.Contains(jstr, asStr) { 40 | t.Errorf("Did not find string ID '%s' in json: %s", asStr, jstr) 41 | } 42 | } 43 | } 44 | 45 | func TestLVTypeJsonUnSerialize(t *testing.T) { 46 | var found disko.LVType 47 | 48 | for asStr, ltype := range valid { 49 | // "4" (no quotes) is valid json rep of int 4. "string" is rep of string. 50 | validJsons := []string{fmt.Sprintf("%d", ltype), "\"" + asStr + "\""} 51 | for _, jsonBlob := range validJsons { 52 | err := json.Unmarshal([]byte(jsonBlob), &found) 53 | if err != nil { 54 | t.Errorf("Failed to unmarshal %s: %s", jsonBlob, err) 55 | } else if found != ltype { 56 | t.Errorf("Unserialized %s, got %d, expected %d", jsonBlob, found, ltype) 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /guid_test.go: -------------------------------------------------------------------------------- 1 | package disko_test 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "machinerun.io/disko" 8 | "machinerun.io/disko/partid" 9 | ) 10 | 11 | func TestStringRoundtrip(t *testing.T) { 12 | guidfmt := "^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$" 13 | matcher := regexp.MustCompile(guidfmt) 14 | myGUID := disko.GenGUID() 15 | 16 | asStr := disko.GUIDToString(myGUID) 17 | 18 | if !matcher.MatchString(asStr) { 19 | t.Errorf( 20 | "guid %#v as a string (%s) did not match format %s", 21 | myGUID, asStr, guidfmt) 22 | } 23 | 24 | back, err := disko.StringToGUID(asStr) 25 | if err != nil { 26 | t.Errorf("StringToGUID failed %#v -> %s: %s)", myGUID, asStr, back) 27 | } 28 | 29 | if back != myGUID { 30 | t.Errorf("Round trip failed. %#v -> %#v", myGUID, back) 31 | } 32 | } 33 | 34 | func TestStringKnown(t *testing.T) { 35 | for _, td := range []struct { 36 | guid disko.GUID 37 | asStr string 38 | }{ 39 | {partid.LinuxFS, "0FC63DAF-8483-4772-8E79-3D69D8477DE4"}, 40 | {disko.GUID{0x67, 0x45, 0x23, 0x1, 0xab, 0x89, 0xef, 0xcd, 0x1, 41 | 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}, 42 | "01234567-89AB-CDEF-0123-456789ABCDEF"}, 43 | } { 44 | found := td.guid.String() 45 | 46 | if found != td.asStr { 47 | t.Errorf("GUIDToString(%#v) got %s. expected %s", 48 | td.guid, found, td.asStr) 49 | } 50 | 51 | back, err := disko.StringToGUID(found) 52 | if err != nil { 53 | t.Errorf("Failed StringToGUID(%#v): %s", found, err) 54 | } 55 | 56 | if td.guid != back { 57 | t.Errorf("StringToGuid(%s) returned %#v. expected %#v", 58 | found, back, td.guid) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package disko 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | ) 7 | 8 | type uRange struct { 9 | Start, Last uint64 10 | } 11 | 12 | func (r *uRange) Size() uint64 { 13 | return r.Last - r.Start + 1 14 | } 15 | 16 | type uRanges []uRange 17 | 18 | func (r uRanges) Len() int { return len(r) } 19 | func (r uRanges) Less(i, j int) bool { return r[i].Start < r[j].Start } 20 | func (r uRanges) Swap(i, j int) { r[i], r[j] = r[j], r[i] } 21 | 22 | // findRangeGaps returns a set of uRange to represent the un-used 23 | // uint64 between min and max that are not included in ranges. 24 | // 25 | // findRangeGaps({{10, 40}, {50, 100}}, 0, 110}) == 26 | // {{0, 9}, {41, 49}, {101, 110}} 27 | // 28 | // Note that input list will be sorted. 29 | func findRangeGaps(ranges uRanges, min, max uint64) uRanges { 30 | // start 'ret' off with full range of min to max, then start cutting it up. 31 | ret := uRanges{{min, max}} 32 | 33 | sort.Sort(ranges) 34 | 35 | for _, i := range ranges { 36 | for r := 0; r < len(ret); r++ { 37 | // 5 cases: 38 | if i.Start > ret[r].Last || i.Last < ret[r].Start { 39 | // a. i has no overlap 40 | } else if i.Start <= ret[r].Start && i.Last >= ret[r].Last { 41 | // b.) i is complete superset, so remove ret[r] 42 | ret = append(ret[:r], ret[r+1:]...) 43 | r-- 44 | } else if i.Start > ret[r].Start && i.Last < ret[r].Last { 45 | // c.) i is strict subset: split ret[r] 46 | ret = append( 47 | append(ret[:r+1], uRange{i.Last + 1, ret[r].Last}), 48 | ret[r+1:]...) 49 | ret[r].Last = i.Start - 1 50 | r++ // added entry is guaranteed to be 'a', so skip it. 51 | } else if i.Start <= ret[r].Start { 52 | // d.) overlap left edge to middle 53 | ret[r].Start = i.Last + 1 54 | } else if i.Start <= ret[r].Last { 55 | // e.) middle to right edge (possibly past). 56 | ret[r].Last = i.Start - 1 57 | } else { 58 | panic(fmt.Sprintf("Error in findRangeGaps: %v, r=%d, ret=%v", 59 | i, r, ret)) 60 | } 61 | } 62 | } 63 | 64 | return ret 65 | } 66 | -------------------------------------------------------------------------------- /linux/virt_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type myDetector struct { 9 | out []byte 10 | err []byte 11 | rc int 12 | expected virtType 13 | logged string 14 | } 15 | 16 | func (d *myDetector) detectVirt() ([]byte, []byte, int) { 17 | return d.out, d.err, d.rc 18 | } 19 | 20 | func (d *myDetector) logf(format string, a ...interface{}) { 21 | d.logged = fmt.Sprintf(format, a...) 22 | } 23 | 24 | func TestVirtType(t *testing.T) { 25 | tables := []myDetector{ 26 | {[]byte("none\n"), []byte{}, 1, virtNone, ""}, 27 | {[]byte("unknown\n"), []byte{}, 0, virtUnknown, ""}, 28 | {[]byte("none\n"), []byte{}, 0, virtNone, ""}, 29 | {[]byte("kvm\n"), []byte{}, 0, virtKvm, ""}, 30 | {[]byte("error\n"), []byte{}, 0, virtError, ""}, 31 | {[]byte("qemu\n"), []byte{}, 0, virtQemu, ""}, 32 | {[]byte("zvm\n"), []byte{}, 0, virtZvm, ""}, 33 | {[]byte("vmware\n"), []byte{}, 0, virtVmware, ""}, 34 | {[]byte("microsoft\n"), []byte{}, 0, virtMicrosoft, ""}, 35 | {[]byte("oracle\n"), []byte{}, 0, virtOracle, ""}, 36 | {[]byte("xen\n"), []byte{}, 0, virtXen, ""}, 37 | {[]byte("bochs\n"), []byte{}, 0, virtBochs, ""}, 38 | {[]byte("uml\n"), []byte{}, 0, virtUml, ""}, 39 | {[]byte("parallels\n"), []byte{}, 0, virtParallels, ""}, 40 | {[]byte("bhyve\n"), []byte{}, 0, virtBhyve, ""}, 41 | {[]byte("not-known-yet\n"), []byte{}, 0, virtUnknown, ""}, 42 | {[]byte("unexpected\n"), []byte("error"), 3, virtError, ""}, 43 | } 44 | 45 | for _, td := range tables { 46 | // set it back to unset so cache is not used. 47 | systemVirtType = virtUnset 48 | td := td 49 | found := getVirtTypeIface(&td) 50 | 51 | if found != td.expected { 52 | t.Errorf("out=%s err=%s rc=%d returned %d (%s) expected %s", 53 | td.out, td.err, td.rc, 54 | found, found.String(), td.expected.String()) 55 | } 56 | } 57 | 58 | systemVirtType = virtOracle 59 | found := getVirtType() 60 | 61 | if found != virtOracle { 62 | t.Errorf("value not cached in systemVirtType. found %s expected %s\n", 63 | fmt.Sprint(found), fmt.Sprint(virtOracle)) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /system.go: -------------------------------------------------------------------------------- 1 | package disko 2 | 3 | // DiskFilter is filter function that returns true if the matching disk is 4 | // accepted false otherwise. 5 | type DiskFilter func(Disk) bool 6 | 7 | // VGFilter is filter function that returns true if the matching vg is 8 | // accepted false otherwise. 9 | type VGFilter func(VG) bool 10 | 11 | // PVFilter is filter function that returns true if the matching pv is 12 | // accepted false otherwise. 13 | type PVFilter func(PV) bool 14 | 15 | // LVFilter is filter function that returns true if the matching lv is 16 | // accepted false otherwise. 17 | type LVFilter func(LV) bool 18 | 19 | // System interface provides system level disk and lvm methods that are 20 | // implemented by the specific system. 21 | type System interface { 22 | // ScanAllDisks scans the system for all available disks and returns a 23 | // set of disks that are accepted by the filter function. Use this function 24 | // if you dont know the device paths for the specific disks to be scanned. 25 | ScanAllDisks(filter DiskFilter) (DiskSet, error) 26 | 27 | // ScanDisks scans the system for disks identified by the specified paths 28 | // and returns a set of disks that are accepted by the filter function. 29 | ScanDisks(filter DiskFilter, paths ...string) (DiskSet, error) 30 | 31 | // ScanDisk scans the system for a single disk specified by the device path. 32 | ScanDisk(path string) (Disk, error) 33 | 34 | // CreatePartition creates a partition on the is disk with the specified 35 | // partition number, type and disk offsets. 36 | CreatePartition(Disk, Partition) error 37 | 38 | // CreatePartitions creates multiple partitions on disk. 39 | CreatePartitions(Disk, PartitionSet) error 40 | 41 | // UpdatePartition updates multiple existing partitions on a disk. 42 | UpdatePartition(Disk, Partition) error 43 | 44 | // UpdatePartitions updates multiple existing partitions on a disk. 45 | UpdatePartitions(Disk, PartitionSet) error 46 | 47 | // DeletePartition deletes the specified partition. 48 | DeletePartition(Disk, uint) error 49 | 50 | // Wipe wipes the disk to make it a clean disk. All partitions and data 51 | // on the disk will be lost. 52 | Wipe(Disk) error 53 | } 54 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | HASH = \# 2 | VERSION := $(shell x=$$(git describe --tags) && echo $${x$(HASH)v} || echo unknown) 3 | VERSION_SUFFIX := $(shell [ -z "$$(git status --porcelain --untracked-files=no)" ] || echo -dirty) 4 | VERSION_FULL := $(VERSION)$(VERSION_SUFFIX) 5 | LDFLAGS := "${ldflags:+$ldflags }-X main.version=${ver}${suff}" 6 | BUILD_FLAGS := -ldflags "-X main.version=$(VERSION_FULL)" 7 | ENV_ROOT := $(shell [ "$$(id -u)" = "0" ] && echo env || echo sudo ) 8 | 9 | GOLANGCI_VER = v1.63.4 10 | GOLANGCI = ./tools/golangci-lint-$(GOLANGCI_VER) 11 | 12 | CMDS := demo/demo ptimg/ptimg 13 | 14 | GO_FILES := $(wildcard *.go) 15 | ALL_GO_FILES := $(wildcard *.go */*.go) 16 | 17 | all: build check 18 | 19 | build: .build $(CMDS) 20 | 21 | .build: $(ALL_GO_FILES) 22 | go build ./... 23 | @touch $@ 24 | 25 | demo/demo: $(wildcard demo/*.go) $(GO_FILES) 26 | cd $(dir $@) && go build $(BUILD_FLAGS) ./... 27 | 28 | ptimg/ptimg: $(wildcard ptimg/*.go) $(GO_FILES) 29 | cd $(dir $@) && go build $(BUILD_FLAGS) ./... 30 | 31 | check: lint gofmt 32 | 33 | gofmt: .gofmt 34 | 35 | .gofmt: $(ALL_GO_FILES) 36 | o=$$(gofmt -s -l -w .) && [ -z "$$o" ] || { echo "gofmt made changes: $$o"; exit 1; } 37 | @touch $@ 38 | 39 | 40 | golangci-lint: $(GOLANGCI) 41 | 42 | $(GOLANGCI): 43 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \ 44 | sh -s -- -b $(dir $@) $(GOLANGCI_VER) || { rm -f $(dir $@)/golangci-lint; exit 1; } 45 | mv $(dir $@)/golangci-lint $@ 46 | 47 | lint: .lint 48 | 49 | .lint: $(ALL_GO_FILES) $(GOLANGCI) 50 | $(GOLANGCI) run ./... 51 | @touch $@ 52 | 53 | test: 54 | go test -v -race -coverprofile=coverage.txt ./... 55 | 56 | test-all: 57 | $(ENV_ROOT) DISKO_INTEGRATION=$${DISKO_INTEGRATION:-run} "GOCACHE=$$(go env GOCACHE)" "GOENV=$$(go env GOENV)" go test -v -coverprofile=coverage-all.tmp -count=1 ./... 58 | @cp coverage-all.tmp coverage-all.txt && rm -f coverage-all.tmp # dance around to not be root-owned 59 | 60 | coverage.html: test 61 | go tool cover -html=coverage.txt -o $@ 62 | 63 | coverage-all.html: test-all 64 | go tool cover -html=coverage-all.txt -o $@ 65 | 66 | debug: 67 | @echo VERSION=$(VERSION) 68 | @echo VERSION_FULL=$(VERSION_FULL) 69 | @echo CMDS=$(CMDS) 70 | 71 | clean: 72 | rm -f $(CMDS) coverage*.txt coverage*.html .lint .build 73 | 74 | .PHONY: debug check test test-all gofmt clean all lint build golangci-lint 75 | -------------------------------------------------------------------------------- /linux/lvm_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "testing" 5 | 6 | "machinerun.io/disko" 7 | ) 8 | 9 | //nolint:funlen 10 | func TestLVDataToLV(t *testing.T) { 11 | var mySize uint64 = 10 * 1024 * 1024 12 | const aUUID = "iFMHAp-24c3-LENS-0IFt-4Mhj-rvhf-kBnnuS" 13 | 14 | for i, d := range []struct { 15 | input lvmLVData 16 | expected disko.LV 17 | }{ 18 | { 19 | input: lvmLVData{ 20 | Name: "myvol0", 21 | VGName: "myvg0", 22 | Path: "/dev/myvg0/myvol0", 23 | Size: mySize, 24 | UUID: aUUID, 25 | Active: true, 26 | Pool: "ThinDataLV", 27 | raw: map[string]string{ 28 | "lv_layout": "linear", 29 | }, 30 | }, 31 | expected: disko.LV{ 32 | Name: "myvol0", 33 | Path: "/dev/myvg0/myvol0", 34 | VGName: "myvg0", 35 | UUID: aUUID, 36 | Size: mySize, 37 | Type: disko.THICK, 38 | Encrypted: false, 39 | }, 40 | }, 41 | { 42 | input: lvmLVData{ 43 | Name: "myvol0", 44 | VGName: "myvg0", 45 | Path: "/dev/myvg0/myvol0", 46 | Size: mySize, 47 | UUID: aUUID, 48 | Active: true, 49 | Pool: "ThinDataLV", 50 | raw: map[string]string{ 51 | "lv_layout": "thin,sparse", 52 | }, 53 | }, 54 | expected: disko.LV{ 55 | Name: "myvol0", 56 | Path: "/dev/myvg0/myvol0", 57 | VGName: "myvg0", 58 | UUID: aUUID, 59 | Size: mySize, 60 | Type: disko.THIN, 61 | Encrypted: false, 62 | }, 63 | }, 64 | { 65 | input: lvmLVData{ 66 | Name: "ThinDataLV", 67 | VGName: "vg_ifc0", 68 | Path: "", 69 | Size: mySize, 70 | UUID: aUUID, 71 | Active: true, 72 | Pool: "", 73 | raw: map[string]string{ 74 | "lv_layout": "thin,pool", 75 | "lv_path": "", 76 | "lv_dm_path": "/dev/mapper/vg_ifc0-ThinDataLV", 77 | "data_lv": "[ThinDataLV_tdata]", 78 | "metadata_lv": "[ThinDataLV_tmeta]", 79 | }, 80 | }, 81 | expected: disko.LV{ 82 | Name: "ThinDataLV", 83 | Path: "", 84 | VGName: "vg_ifc0", 85 | UUID: aUUID, 86 | Size: mySize, 87 | Type: disko.THINPOOL, 88 | Encrypted: false, 89 | }, 90 | }, 91 | } { 92 | found := d.input.toLV() 93 | if found != d.expected { 94 | t.Errorf("entry %d found != expected\n%v\n%v\n", i, found, d.expected) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /mockos/testdata/model_sys.json: -------------------------------------------------------------------------------- 1 | { 2 | "disks": { 3 | "sda": { 4 | "name": "sda", 5 | "path": "/dev/sda", 6 | "size": 214748364800, 7 | "sectorSize": 512, 8 | "freeSpace": [], 9 | "type": 1, 10 | "attachment": 3, 11 | "partitions": {}, 12 | "UdevInfo": {} 13 | }, 14 | "sdb": { 15 | "name": "sdb", 16 | "path": "/dev/sdb", 17 | "size": 322122547200, 18 | "sectorSize": 512, 19 | "freeSpace": [], 20 | "type": 1, 21 | "attachment": 1, 22 | "partitions": {}, 23 | "UdevInfo": {} 24 | }, 25 | "sdc": { 26 | "name": "sdc", 27 | "path": "/dev/sdc", 28 | "size": 2638827906662, 29 | "sectorSize": 512, 30 | "freeSpace": [], 31 | "type": 0, 32 | "attachment": 1, 33 | "partitions": {}, 34 | "UdevInfo": {} 35 | }, 36 | "sdd": { 37 | "name": "sdd", 38 | "path": "/dev/sdd", 39 | "size": 2638827906662, 40 | "sectorSize": 512, 41 | "freeSpace": [], 42 | "type": 0, 43 | "attachment": 1, 44 | "partitions": {}, 45 | "UdevInfo": {} 46 | }, 47 | "sde": { 48 | "name": "sde", 49 | "path": "/dev/sde", 50 | "size": 2638827906662, 51 | "sectorSize": 512, 52 | "freeSpace": [], 53 | "type": 0, 54 | "attachment": 1, 55 | "partitions": {}, 56 | "UdevInfo": {} 57 | }, 58 | "sdf": { 59 | "name": "sdf", 60 | "path": "/dev/sdf", 61 | "size": 2638827906662, 62 | "sectorSize": 512, 63 | "freeSpace": [], 64 | "type": 0, 65 | "attachment": 1, 66 | "partitions": {}, 67 | "UdevInfo": {} 68 | }, 69 | "sdg": { 70 | "name": "sdg", 71 | "path": "/dev/sdg", 72 | "size": 322122547200, 73 | "sectorSize": 512, 74 | "freeSpace": [], 75 | "type": 1, 76 | "attachment": 1, 77 | "partitions": {}, 78 | "UdevInfo": {} 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /linux/virt.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import "log" 4 | 5 | type virtType int 6 | 7 | const ( 8 | virtUnset virtType = iota 9 | virtUnknown 10 | virtNone 11 | virtKvm 12 | virtError 13 | virtQemu 14 | virtZvm 15 | virtVmware 16 | virtMicrosoft 17 | virtOracle 18 | virtXen 19 | virtBochs 20 | virtUml 21 | virtParallels 22 | virtBhyve 23 | ) 24 | 25 | var systemVirtType = virtUnset //nolint:gochecknoglobals 26 | 27 | var virtTypesToString = map[virtType]string{ //nolint:gochecknoglobals 28 | virtUnset: "unset", 29 | virtUnknown: "unknown", 30 | virtNone: "none", 31 | virtKvm: "kvm", 32 | virtError: "error", 33 | virtQemu: "qemu", 34 | virtZvm: "zvm", 35 | virtVmware: "vmware", 36 | virtMicrosoft: "microsoft", 37 | virtOracle: "oracle", 38 | virtXen: "xen", 39 | virtBochs: "bochs", 40 | virtUml: "uml", 41 | virtParallels: "parallels", 42 | virtBhyve: "bhyve", 43 | } 44 | 45 | func (t *virtType) String() string { 46 | return virtTypesToString[*t] 47 | } 48 | 49 | type detector interface { 50 | detectVirt() ([]byte, []byte, int) 51 | logf(string, ...interface{}) 52 | } 53 | 54 | type systemdDetector struct { 55 | } 56 | 57 | func (d *systemdDetector) detectVirt() ([]byte, []byte, int) { 58 | return runCommandWithOutputErrorRc("systemd-detect-virt", "--vm") 59 | } 60 | 61 | func (d *systemdDetector) logf(format string, a ...interface{}) { 62 | log.Printf(format, a...) 63 | } 64 | 65 | func getVirtType() virtType { 66 | return getVirtTypeIface(&systemdDetector{}) 67 | } 68 | 69 | func getVirtTypeIface(sdv detector) virtType { 70 | if systemVirtType != virtUnset { 71 | return systemVirtType 72 | } 73 | 74 | out, stderr, rc := sdv.detectVirt() 75 | 76 | if rc == 0 || rc == 1 { 77 | var strOut string 78 | if len(out) > 1 { 79 | strOut = string(out[:len(out)-1]) 80 | } 81 | 82 | for t, s := range virtTypesToString { 83 | if strOut == s { 84 | systemVirtType = t 85 | break 86 | } 87 | } 88 | 89 | if systemVirtType == virtUnset { 90 | sdv.logf("Unknown virt type: %s/%s", strOut, string(stderr)) 91 | 92 | systemVirtType = virtUnknown 93 | } 94 | } else { 95 | sdv.logf("Failed to read virt type [%d]: %s/%s", 96 | rc, string(out), string(stderr)) 97 | systemVirtType = virtError 98 | } 99 | 100 | return systemVirtType 101 | } 102 | 103 | func isKvm() bool { 104 | return getVirtType() == virtKvm 105 | } 106 | -------------------------------------------------------------------------------- /mpi3mr/mpi3mr.go: -------------------------------------------------------------------------------- 1 | package mpi3mr 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | "machinerun.io/disko" 9 | ) 10 | 11 | // Controller - a mpi3mr controller 12 | type Controller struct { 13 | ID int `json:"ID"` 14 | PhysicalDrives PhysicalDriveSet `json:"PhysicalDrives"` 15 | VirtualDrives VirtualDriveSet `json:"VirtualDrives"` 16 | } 17 | 18 | type PhysicalDriveSet map[string]PhysicalDrive 19 | type VirtualDriveSet map[string]VirtualDrive 20 | 21 | // IsSSD - is this drive group composed of all SSD 22 | func (vd *VirtualDrive) IsSSD() bool { 23 | if len(vd.PhysicalDrives) == 0 { 24 | return false 25 | } 26 | for pID := range vd.PhysicalDrives { 27 | if vd.PhysicalDrives[pID].Medium != "SSD" { 28 | return false 29 | } 30 | } 31 | return true 32 | } 33 | 34 | // IsEqual - compare the two drives 35 | func (vd *VirtualDrive) IsEqual(vd2 VirtualDrive) bool { 36 | return cmp.Equal(*vd, vd2) 37 | } 38 | 39 | // MediaType - a disk "Media" 40 | type MediaType int 41 | 42 | const ( 43 | // UnknownMedia - indicates an unknown media 44 | UnknownMedia MediaType = iota 45 | 46 | // HDD - Spinning hard disk. 47 | HDD 48 | 49 | // SSD - Solid State Disk 50 | SSD 51 | ) 52 | 53 | func (t MediaType) String() string { 54 | return []string{"UNKNOWN", "HDD", "SSD"}[t] 55 | } 56 | 57 | // MarshalJSON for string output rather than int 58 | func (t MediaType) MarshalJSON() ([]byte, error) { 59 | return json.Marshal(t.String()) 60 | } 61 | 62 | // Mpi3mr - basic interface 63 | type Mpi3mr interface { 64 | // List - Return list of Controller IDs 65 | List() ([]int, error) 66 | 67 | // Query - Query the controller provided 68 | Query(int) (Controller, error) 69 | 70 | // GetDiskType - Determine the disk type if controller owns disk 71 | GetDiskType(string) (disko.DiskType, error) 72 | 73 | // DriverSysfsPath - Return the sysfs path to the linux driver for this controller 74 | DriverSysfsPath() string 75 | 76 | // IsSysPathRAID - Check if sysfs path is a device on the controller 77 | IsSysPathRAID(string) bool 78 | } 79 | 80 | // ErrNoController - Error reported by Query if no controller is found. 81 | var ErrNoController = errors.New("mpi3mr Controller not found") 82 | 83 | // ErrUnsupported - Error reported by Query if controller is not supported. 84 | var ErrUnsupported = errors.New("mpi3mr Controller unsupported") 85 | 86 | // ErrNoStor2cli - Error reported by Query if no storcli binary in PATH 87 | var ErrNoStor2cli = errors.New("no 'storcli2' command in PATH") 88 | -------------------------------------------------------------------------------- /demo/megaraid.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/urfave/cli/v2" 9 | "machinerun.io/disko/linux" 10 | "machinerun.io/disko/megaraid" 11 | ) 12 | 13 | //nolint:gochecknoglobals 14 | var megaraidCommands = cli.Command{ 15 | Name: "megaraid", 16 | Usage: "megaraid / storcli commands", 17 | Subcommands: []*cli.Command{ 18 | { 19 | Name: "dump", 20 | Usage: "Dump information about megaraid", 21 | Action: megaraidDump, 22 | }, 23 | { 24 | Name: "disk-summary", 25 | Usage: "Show information about virtual devices on system", 26 | Action: megaraidDiskSummary, 27 | }, 28 | }, 29 | } 30 | 31 | const ( 32 | HDD = "HDD" 33 | SSD = "SSD" 34 | ) 35 | 36 | func megaraidDiskSummary(c *cli.Context) error { 37 | var err error 38 | var ctrlNum = 0 39 | var ctrlArg = c.Args().First() 40 | 41 | if ctrlArg != "" { 42 | ctrlNum, err = strconv.Atoi(ctrlArg) 43 | if err != nil { 44 | return fmt.Errorf("could not convert to integer: %s", err) 45 | } 46 | } 47 | 48 | mraid := megaraid.StorCli() 49 | ctrl, err := mraid.Query(ctrlNum) 50 | 51 | if err != nil { 52 | return err 53 | } 54 | 55 | data := [][]string{{"Path", "Name", "Type", "State"}} 56 | 57 | for _, vd := range ctrl.VirtDrives { 58 | stype := HDD 59 | 60 | if ctrl.DriveGroups[vd.DriveGroup].IsSSD() { 61 | stype = SSD 62 | } 63 | 64 | name := vd.RaidName 65 | if vd.RaidName == "" { 66 | name = fmt.Sprintf("virtid-%d", vd.ID) 67 | } 68 | 69 | data = append(data, []string{vd.Path, name, stype, vd.Raw["State"]}) 70 | } 71 | 72 | for _, d := range ctrl.Drives { 73 | if d.DriveGroup >= 0 { 74 | continue 75 | } 76 | 77 | path := "" 78 | if bname, err := linux.NameByDiskID(mraid.DriverSysfsPath(), d.ID); err == nil { 79 | path = "/dev/" + bname 80 | } 81 | 82 | data = append(data, []string{path, fmt.Sprintf("diskid-%d", d.ID), 83 | d.MediaType.String(), d.State}) 84 | } 85 | 86 | printTextTable(data) 87 | 88 | return nil 89 | } 90 | 91 | func megaraidDump(c *cli.Context) error { 92 | var err error 93 | var ctrlNum = 0 94 | var ctrlArg = c.Args().First() 95 | 96 | if ctrlArg != "" { 97 | ctrlNum, err = strconv.Atoi(ctrlArg) 98 | if err != nil { 99 | return fmt.Errorf("could not convert to integer: %s", err) 100 | } 101 | } 102 | 103 | mraid := megaraid.StorCli() 104 | ctrl, err := mraid.Query(ctrlNum) 105 | 106 | if err != nil { 107 | return err 108 | } 109 | 110 | jbytes, err := json.MarshalIndent(&ctrl, "", " ") 111 | if err != nil { 112 | return err 113 | } 114 | 115 | fmt.Printf("%s\n", string(jbytes)) 116 | 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /demo/smartpqi.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/urfave/cli/v2" 9 | "machinerun.io/disko/smartpqi" 10 | ) 11 | 12 | //nolint:gochecknoglobals 13 | var smartpqiCommands = cli.Command{ 14 | Name: "smartpqi", 15 | Usage: "smartpqi / arcconf commands", 16 | Subcommands: []*cli.Command{ 17 | { 18 | Name: "dump", 19 | Usage: "Dump information about smartpqi", 20 | Action: smartpqiDump, 21 | }, 22 | { 23 | Name: "disk-summary", 24 | Usage: "Show information about virtual devices on system", 25 | Action: smartpqiDiskSummary, 26 | }, 27 | { 28 | Name: "list-controllers", 29 | Usage: "Show the discovered controller IDs", 30 | Action: smartpqiListControllers, 31 | }, 32 | }, 33 | } 34 | 35 | func smartpqiListControllers(c *cli.Context) error { 36 | arc := smartpqi.ArcConf() 37 | ctrls, err := arc.List() 38 | if err != nil { 39 | return fmt.Errorf("failed to list controllers: %s", err) 40 | } 41 | 42 | fmt.Printf("Found %d controllers.", len(ctrls)) 43 | for _, cID := range ctrls { 44 | fmt.Printf("Controller ID: %d\n", cID) 45 | } 46 | return nil 47 | } 48 | 49 | func smartpqiDiskSummary(c *cli.Context) error { 50 | var err error 51 | var ctrlNum = 1 52 | var ctrlArg = c.Args().First() 53 | 54 | if ctrlArg != "" { 55 | ctrlNum, err = strconv.Atoi(ctrlArg) 56 | if err != nil { 57 | return fmt.Errorf("could not convert to integer: %s", err) 58 | } 59 | } 60 | 61 | arc := smartpqi.ArcConf() 62 | ctrl, err := arc.Query(ctrlNum) 63 | 64 | if err != nil { 65 | return err 66 | } 67 | 68 | data := [][]string{{"Path", "Name", "DiskType", "RAID"}} 69 | 70 | for _, ld := range ctrl.LogicalDrives { 71 | stype := "HDD" 72 | 73 | if ld.IsSSD() { 74 | stype = "SSD" 75 | } 76 | 77 | name := ld.Name 78 | if ld.Name == "" { 79 | name = fmt.Sprintf("logicalid-%d", ld.ID) 80 | } 81 | 82 | data = append(data, []string{ld.DiskName, name, stype, ld.RAIDLevel}) 83 | } 84 | 85 | printTextTable(data) 86 | 87 | return nil 88 | } 89 | 90 | func smartpqiDump(c *cli.Context) error { 91 | var err error 92 | var ctrlNum = 1 93 | var ctrlArg = c.Args().First() 94 | 95 | if ctrlArg != "" { 96 | ctrlNum, err = strconv.Atoi(ctrlArg) 97 | if err != nil { 98 | return fmt.Errorf("could not convert to integer: %s", err) 99 | } 100 | } 101 | 102 | arc := smartpqi.ArcConf() 103 | ctrl, err := arc.Query(ctrlNum) 104 | 105 | if err != nil { 106 | return err 107 | } 108 | 109 | jbytes, err := json.MarshalIndent(&ctrl, "", " ") 110 | if err != nil { 111 | return err 112 | } 113 | 114 | fmt.Printf("%s\n", string(jbytes)) 115 | 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /linux/lvmdump_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var size1 uint64 = 27514634240 11 | var size2 uint64 = 55029268480 12 | var size3 = size2 * 2 13 | 14 | func asBS(b uint64) string { 15 | return fmt.Sprintf("%dB", b) 16 | } 17 | 18 | func TestParseLvReport(t *testing.T) { 19 | ast := assert.New(t) 20 | rawStub := map[string]string{"ignore-key": "ignore-val"} 21 | 22 | found, err := parseLvReport([]byte( 23 | `{"report": [{"lv": [{ 24 | "lv_active": "active", 25 | "lv_full_name": "atx_container/storage", 26 | "lv_name": "storage", 27 | "lv_path": "/dev/atx_container/storage", 28 | "lv_size": "` + asBS(size1) + `", 29 | "lv_uuid": "yY7AfO-dtWE-ROJR-f7G9-d70P-pjGF-lFfXgf", 30 | "vg_name": "atx_container", 31 | "pool_lv": "" 32 | }]}]}`)) 33 | found[0].raw = rawStub 34 | 35 | ast.Equal(nil, err) 36 | ast.Equal( 37 | []lvmLVData{ 38 | { 39 | Name: "storage", 40 | VGName: "atx_container", 41 | Path: "/dev/atx_container/storage", 42 | Size: size1, 43 | UUID: "yY7AfO-dtWE-ROJR-f7G9-d70P-pjGF-lFfXgf", 44 | Active: true, 45 | Pool: "", 46 | raw: rawStub, 47 | }}, found) 48 | } 49 | 50 | func TestParseVgReport(t *testing.T) { 51 | ast := assert.New(t) 52 | rawStub := map[string]string{"ignore-key": "ignore-val"} 53 | found, err := parseVgReport([]byte( 54 | `{"report": [{"vg": [{ 55 | "lv_count": "1", 56 | "pv_count": "1", 57 | "vg_free": "0B", 58 | "vg_name": "atx_container", 59 | "vg_size": "` + asBS(size2) + `", 60 | "vg_uuid": "pB0WKT-WukN-IAjl-Q1Lr-bLmH-Xh5x-In0V5e" 61 | }]}]}`)) 62 | found[0].raw = rawStub 63 | 64 | ast.Equal(nil, err) 65 | ast.Equal( 66 | []lvmVGData{ 67 | { 68 | Name: "atx_container", 69 | Size: size2, 70 | UUID: "pB0WKT-WukN-IAjl-Q1Lr-bLmH-Xh5x-In0V5e", 71 | Free: 0, 72 | raw: rawStub, 73 | }}, found) 74 | } 75 | 76 | func TestParsePvReport(t *testing.T) { 77 | ast := assert.New(t) 78 | rawStub := map[string]string{"ignore-key": "ignore-val"} 79 | found, err := parsePvReport([]byte( 80 | `{"report": [{"pv": [{ 81 | "dev_size": "` + asBS(size2) + `", 82 | "pv_free": "` + asBS(size3) + `", 83 | "pv_mda_size": "` + asBS(size1) + `", 84 | "pv_name": "/dev/vda3", 85 | "pv_size": "` + asBS(size2) + `", 86 | "pv_uuid": "Gf0GD0-hH0M-7x8i-9LQt-AAZm-ke5b-VfWlGR", 87 | "vg_name": "vg0" 88 | }]}]}`)) 89 | found[0].raw = rawStub 90 | 91 | ast.Equal(nil, err) 92 | ast.Equal( 93 | []lvmPVData{ 94 | { 95 | Path: "/dev/vda3", 96 | VGName: "vg0", 97 | Size: size2, 98 | UUID: "Gf0GD0-hH0M-7x8i-9LQt-AAZm-ke5b-VfWlGR", 99 | Free: size3, 100 | MetadataSize: size1, 101 | raw: rawStub, 102 | }}, found) 103 | } 104 | -------------------------------------------------------------------------------- /demo/mpi3mr.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/urfave/cli/v2" 10 | "machinerun.io/disko/linux" 11 | "machinerun.io/disko/mpi3mr" 12 | ) 13 | 14 | //nolint:gochecknoglobals 15 | var mpi3mrCommands = cli.Command{ 16 | Name: "mpi3mr", 17 | Usage: "mpi3mr / storcli2 commands", 18 | Subcommands: []*cli.Command{ 19 | { 20 | Name: "dump", 21 | Usage: "Dump information about mpi3mr", 22 | Action: mpi3mrDump, 23 | }, 24 | { 25 | Name: "disk-summary", 26 | Usage: "Show information about virtual disk devices on system", 27 | Action: mpi3mrDiskSummary, 28 | }, 29 | { 30 | Name: "list-controllers", 31 | Usage: "Show the discovered controllers' ID", 32 | Action: mpi3mrListControllers, 33 | }, 34 | }, 35 | } 36 | 37 | func mpi3mrListControllers(c *cli.Context) error { 38 | stor := mpi3mr.StorCli2() 39 | ctrls, err := stor.List() 40 | if err != nil { 41 | return errors.Errorf("failed to list controllers: %v", err) 42 | } 43 | 44 | fmt.Printf("Found %d controllers:\n", len(ctrls)) 45 | for _, cID := range ctrls { 46 | fmt.Printf("Controller ID: %d\n", cID) 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func mpi3mrDiskSummary(c *cli.Context) error { 53 | var err error 54 | var ctrlNum = 0 55 | var ctrlArg = c.Args().First() 56 | 57 | if ctrlArg != "" { 58 | ctrlNum, err = strconv.Atoi(ctrlArg) 59 | if err != nil { 60 | return errors.Errorf("invalid controller number: %v", err) 61 | } 62 | } 63 | 64 | stor := mpi3mr.StorCli2() 65 | ctrl, err := stor.Query(ctrlNum) 66 | if err != nil { 67 | return errors.Errorf("failed to query controller %d: %v", ctrlNum, err) 68 | } 69 | 70 | data := [][]string{{"Path", "Name", "Type", "State"}} 71 | 72 | for _, vd := range ctrl.VirtualDrives { 73 | stype := "HDD" 74 | 75 | if vd.IsSSD() { 76 | stype = "SSD" 77 | } 78 | 79 | name := vd.Name 80 | if vd.Name == "" { 81 | name = fmt.Sprintf("virtid-%s", vd.ID()) 82 | } 83 | 84 | data = append(data, []string{vd.Path(), name, stype, vd.State}) 85 | } 86 | 87 | for _, d := range ctrl.PhysicalDrives { 88 | if d.DG >= 0 { 89 | continue 90 | } 91 | 92 | path := "" 93 | if bname, err := linux.NameByDiskID(stor.DriverSysfsPath(), d.PID); err == nil { 94 | path = "/dev/" + bname 95 | } 96 | 97 | data = append(data, []string{path, fmt.Sprintf("diskid-%d", d.PID), 98 | d.Medium, d.State}) 99 | } 100 | 101 | printTextTable(data) 102 | return nil 103 | } 104 | 105 | func mpi3mrDump(c *cli.Context) error { 106 | var err error 107 | var ctrlNum = 0 108 | var ctrlArg = c.Args().First() 109 | 110 | if ctrlArg != "" { 111 | ctrlNum, err = strconv.Atoi(ctrlArg) 112 | if err != nil { 113 | return errors.Errorf("invalid controller number: %v", err) 114 | } 115 | } 116 | 117 | stor := mpi3mr.StorCli2() 118 | ctrl, err := stor.Query(ctrlNum) 119 | if err != nil { 120 | return errors.Errorf("failed to query controller %d: %v", ctrlNum, err) 121 | } 122 | 123 | jbytes, err := json.MarshalIndent(&ctrl, "", " ") 124 | if err != nil { 125 | return errors.Errorf("failed to marshal controller: %v", err) 126 | } 127 | 128 | fmt.Printf("%s\n", string(jbytes)) 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /mockos/system.go: -------------------------------------------------------------------------------- 1 | package mockos 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "machinerun.io/disko" 9 | ) 10 | 11 | // System returns a mock os implementation of the disk.System interface. 12 | func System(layout string) disko.System { 13 | file, err := os.ReadFile(layout) 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | sys := &mockSys{} 19 | 20 | if err := json.Unmarshal(file, sys); err != nil { 21 | panic(err) 22 | } 23 | 24 | return sys 25 | } 26 | 27 | type mockSys struct { 28 | Disks disko.DiskSet `json:"disks"` 29 | } 30 | 31 | func (ms *mockSys) ScanAllDisks(filter disko.DiskFilter) (disko.DiskSet, error) { 32 | disks := disko.DiskSet{} 33 | 34 | for n, d := range ms.Disks { 35 | if filter == nil || filter(d) { 36 | disks[n] = d 37 | } 38 | } 39 | 40 | return disks, nil 41 | } 42 | 43 | func (ms *mockSys) ScanDisks(filter disko.DiskFilter, paths ...string) (disko.DiskSet, error) { 44 | disks := disko.DiskSet{} 45 | 46 | for _, p := range paths { 47 | d, e := ms.ScanDisk(p) 48 | 49 | if e != nil { 50 | return nil, e 51 | } 52 | 53 | if filter(d) { 54 | disks[d.Name] = d 55 | } 56 | } 57 | 58 | return disks, nil 59 | } 60 | 61 | func (ms *mockSys) ScanDisk(path string) (disko.Disk, error) { 62 | // Find the disk from the disk set 63 | for _, d := range ms.Disks { 64 | if d.Path == path { 65 | return d, nil 66 | } 67 | } 68 | 69 | return disko.Disk{}, fmt.Errorf("disk %s not found", path) 70 | } 71 | 72 | func (ms *mockSys) CreatePartition(d disko.Disk, p disko.Partition) error { 73 | if disk, ok := ms.Disks[d.Name]; ok { 74 | if _, ok := disk.Partitions[p.Number]; ok { 75 | return fmt.Errorf("partition %d already exists", p.Number) 76 | } 77 | 78 | disk.Partitions[p.Number] = p 79 | 80 | // Ignore free spaces for mock 81 | return nil 82 | } 83 | 84 | return fmt.Errorf("disk %s does not exist", d.Name) 85 | } 86 | 87 | func (ms *mockSys) CreatePartitions(d disko.Disk, pSet disko.PartitionSet) error { 88 | for _, p := range pSet { 89 | if err := ms.CreatePartition(d, p); err != nil { 90 | return err 91 | } 92 | } 93 | 94 | return nil 95 | } 96 | 97 | func (ms *mockSys) UpdatePartition(d disko.Disk, p disko.Partition) error { 98 | cur, ok := d.Partitions[p.Number] 99 | 100 | if !ok { 101 | return fmt.Errorf("partition %d did not exist on disk %s", p.Number, d) 102 | } 103 | 104 | emptyGUID := disko.GUID{} 105 | emptyType := disko.PartType{} 106 | upd := cur 107 | 108 | if p.Name != "" { 109 | upd.Name = p.Name 110 | } 111 | 112 | if p.ID != emptyGUID { 113 | upd.ID = p.ID 114 | } 115 | 116 | if p.Type != emptyType { 117 | upd.Type = p.Type 118 | } 119 | 120 | d.Partitions[p.Number] = upd 121 | 122 | return nil 123 | } 124 | 125 | func (ms *mockSys) UpdatePartitions(d disko.Disk, pSet disko.PartitionSet) error { 126 | for _, p := range pSet { 127 | if err := ms.UpdatePartition(d, p); err != nil { 128 | return err 129 | } 130 | } 131 | 132 | return nil 133 | } 134 | 135 | func (ms *mockSys) DeletePartition(d disko.Disk, number uint) error { 136 | if disk, ok := ms.Disks[d.Name]; ok { 137 | if _, ok := disk.Partitions[number]; !ok { 138 | return fmt.Errorf("partition %d does not exist", number) 139 | } 140 | 141 | delete(disk.Partitions, number) 142 | 143 | // Ignore free space for mock 144 | return nil 145 | } 146 | 147 | return fmt.Errorf("disk %s does not exist", d.Name) 148 | } 149 | 150 | func (ms *mockSys) Wipe(d disko.Disk) error { 151 | // later mate 152 | return nil 153 | } 154 | -------------------------------------------------------------------------------- /smartpqi/smartpqi.go: -------------------------------------------------------------------------------- 1 | package smartpqi 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "strings" 7 | 8 | "machinerun.io/disko" 9 | ) 10 | 11 | type Controller struct { 12 | ID int 13 | PhysicalDrives DriveSet 14 | LogicalDrives LogicalDriveSet 15 | } 16 | 17 | type PhysicalDevice struct { 18 | ArrayID int `json:"ArrayID"` 19 | Availability string `json:"Availability"` 20 | BlockSize int `json:"BlockSize"` 21 | Channel int `json:"Channel"` 22 | ID int `json:"ID"` 23 | Firmware string `json:"Firmware"` 24 | Model string `json:"Model"` 25 | PhysicalBlockSize int `json:"PhysicalBlockSize"` 26 | Protocol string `json:"Protocol"` 27 | SerialNumber string `json:"SerialNumber"` 28 | SizeMB int `json:"SizeMB"` 29 | Type MediaType `json:"Type"` 30 | Vendor string `json:"Vendor"` 31 | WriteCache string `json:"WriteCache"` 32 | } 33 | 34 | type LogicalDevice struct { 35 | ArrayID int `json:"ArrayID"` 36 | BlockSize int `json:"BlockSize"` 37 | Caching string `json:"Caching"` 38 | Devices []*PhysicalDevice 39 | DiskName string `json:"DiskName"` 40 | ID int `json:"ID"` 41 | InterfaceType string `json:"InterfaceType"` 42 | Name string `json:"Name"` 43 | RAIDLevel string `json:"RAIDLevel"` 44 | SizeMB int `json:"SizeMB"` 45 | } 46 | 47 | // IsSSD - is this logical device composed of all SSD 48 | func (ld *LogicalDevice) IsSSD() bool { 49 | if len(ld.Devices) == 0 { 50 | return false 51 | } 52 | 53 | for _, pDev := range ld.Devices { 54 | if pDev.Type != SSD { 55 | return false 56 | } 57 | } 58 | 59 | return true 60 | } 61 | 62 | type DriveSet map[int]*PhysicalDevice 63 | 64 | type LogicalDriveSet map[int]*LogicalDevice 65 | 66 | // MediaType 67 | type MediaType int 68 | 69 | const ( 70 | // UnknownMedia - indicates an unknown media 71 | UnknownMedia MediaType = iota 72 | 73 | // HDD - Spinning hard disk. 74 | HDD 75 | 76 | // SSD - Solid State Disk 77 | SSD 78 | 79 | // NVME - Non Volatile Memory Express 80 | NVME 81 | ) 82 | 83 | func (t MediaType) String() string { 84 | return []string{"UNKNOWN", "HDD", "SSD", "NVME"}[t] 85 | } 86 | 87 | func GetMediaType(mediaType string) MediaType { 88 | switch strings.ToUpper(mediaType) { 89 | case "HDD": 90 | return HDD 91 | case "SSD": 92 | return SSD 93 | case "NVME": 94 | return NVME 95 | default: 96 | return UnknownMedia 97 | } 98 | } 99 | 100 | // MarshalJSON for string output rather than int 101 | func (t MediaType) MarshalJSON() ([]byte, error) { 102 | return json.Marshal(t.String()) 103 | } 104 | 105 | func (t *MediaType) UnmarshalJSON(data []byte) error { 106 | var mt string 107 | if err := json.Unmarshal(data, &mt); err != nil { 108 | return err 109 | } 110 | *t = GetMediaType(mt) 111 | return nil 112 | } 113 | 114 | // SmartPqi - basic interface 115 | type SmartPqi interface { 116 | // List - Return list of Controller IDs 117 | List() ([]int, error) 118 | 119 | // Query - Query the controller provided 120 | Query(int) (Controller, error) 121 | 122 | // GetDiskType - Determine the disk type if controller owns disk 123 | GetDiskType(string) (disko.DiskType, error) 124 | 125 | // DriverSysfsPath - Return the sysfs path to the linux driver for this controller 126 | DriverSysfsPath() string 127 | 128 | // IsSysPathRAID - Check if sysfs path is a device on the controller 129 | IsSysPathRAID(string) bool 130 | } 131 | 132 | // ErrNoController - Error reported by Query if no controller is found. 133 | var ErrNoController = errors.New("smartpqi Controller not found") 134 | 135 | // ErrUnsupported - Error reported by Query if controller is not supported. 136 | var ErrUnsupported = errors.New("smartpqi Controller unsupported") 137 | 138 | // ErrNoArcconf - Error reported by Query if no arcconf binary in PATH 139 | var ErrNoArcconf = errors.New("no 'arcconf' command in PATH") 140 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 7 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 8 | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= 9 | github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= 10 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 11 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 12 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 13 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 14 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 15 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/rekby/gpt v0.0.0-20200614112001-7da10aec5566 h1:U4d0m0NdADC5sjaWXeZpDZ/TFvE866u1Js5yP3M3mho= 19 | github.com/rekby/gpt v0.0.0-20200614112001-7da10aec5566/go.mod h1:scrOqOnnHVKCHENvFw8k9ajCb88uqLQDA4BvuJNJ2ew= 20 | github.com/rekby/mbr v0.0.0-20190325193910-2b19b9cdeebc h1:LIhcsQ01OzuCmjqcggpWhs8GBGNqVPycFbBpY3suBbI= 21 | github.com/rekby/mbr v0.0.0-20190325193910-2b19b9cdeebc/go.mod h1:omSwqul59wlKxf3OVbxhOiSjxM1at3GsfDbgnghKyeA= 22 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 23 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 24 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 25 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 26 | github.com/smartystreets/assertions v1.13.1 h1:Ef7KhSmjZcK6AVf9YbJdvPYG9avaF0ZxudX+ThRdWfU= 27 | github.com/smartystreets/assertions v1.13.1/go.mod h1:cXr/IwVfSo/RbCSPhoAPv73p3hlSdrBH/b3SdnW/LMY= 28 | github.com/smartystreets/goconvey v1.8.0 h1:Oi49ha/2MURE0WexF052Z0m+BNSGirfjg5RL+JXWq3w= 29 | github.com/smartystreets/goconvey v1.8.0/go.mod h1:EdX8jtrTIj26jmjCOVNMVSIYAtgexqXKHOXW2Dx9JLg= 30 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 31 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 32 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 33 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 34 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 35 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 36 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 37 | github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY= 38 | github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= 39 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 40 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 41 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 42 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 45 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 46 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 47 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 48 | -------------------------------------------------------------------------------- /megaraid/megaraid.go: -------------------------------------------------------------------------------- 1 | package megaraid 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | 7 | "machinerun.io/disko" 8 | ) 9 | 10 | // Controller - a Megaraid controller 11 | type Controller struct { 12 | ID int 13 | Drives DriveSet 14 | DriveGroups DriveGroupSet 15 | VirtDrives VirtDriveSet 16 | } 17 | 18 | // VirtDrive - represents a virtual drive. 19 | type VirtDrive struct { 20 | // the Virtual Drive Number / ID 21 | ID int 22 | // the Drive Group ID 23 | DriveGroup int 24 | // Path in linux - may be empty if "Exposed to OS" != "Yes" 25 | Path string 26 | // "Name" in output - exposed in cimc UI 27 | RaidName string 28 | // Type RAID type (RAID0, RAID1...) 29 | Type string 30 | // /c0/v0 data as a string map 31 | Raw map[string]string 32 | // "VD0 Properties" as a map 33 | Properties map[string]string 34 | } 35 | 36 | // VirtDriveSet - a map of VirtDrives by their Number. 37 | type VirtDriveSet map[int]*VirtDrive 38 | 39 | // DriveGroup - a megaraid "Drive Group". These really have nothing 40 | // that is their own other than their ID. 41 | type DriveGroup struct { 42 | ID int 43 | Drives DriveSet 44 | } 45 | 46 | // IsSSD - is this drive group composed of all SSD 47 | func (dg *DriveGroup) IsSSD() bool { 48 | if len(dg.Drives) == 0 { 49 | return false 50 | } 51 | 52 | for _, drive := range dg.Drives { 53 | if drive.MediaType != SSD { 54 | return false 55 | } 56 | } 57 | 58 | return true 59 | } 60 | 61 | // DriveGroupSet - map of DriveGroups by their ID 62 | type DriveGroupSet map[int]*DriveGroup 63 | 64 | // MarshalJSON - serialize to json. Custom Marshal to only reference Disks 65 | // by ID not by full dump. 66 | func (dgs DriveGroupSet) MarshalJSON() ([]byte, error) { 67 | type terseDriveGroupSet struct { 68 | ID int 69 | Drives []int 70 | } 71 | 72 | var mySet = []terseDriveGroupSet{} 73 | 74 | for id, driveSet := range dgs { 75 | drives := []int{} 76 | for drive := range driveSet.Drives { 77 | drives = append(drives, drive) 78 | } 79 | 80 | mySet = append(mySet, terseDriveGroupSet{ID: id, Drives: drives}) 81 | } 82 | 83 | return json.Marshal(&mySet) 84 | } 85 | 86 | // Drive - a megaraid (physical) Drive. 87 | type Drive struct { 88 | ID int 89 | DriveGroup int 90 | EID int 91 | Slot int 92 | State string 93 | MediaType MediaType 94 | Model string 95 | Raw map[string]string 96 | } 97 | 98 | // DriveSet - just a map of Drives by ID 99 | type DriveSet map[int]*Drive 100 | 101 | // IsEqual - compare the two drives (does not compare Raw) 102 | func (d *Drive) IsEqual(d2 Drive) bool { 103 | return (d.ID == d2.ID && d.EID == d2.EID && d.Slot == d2.Slot && 104 | d.DriveGroup == d2.DriveGroup && d.State == d2.State && 105 | d.MediaType == d2.MediaType && 106 | d.Model == d2.Model) 107 | } 108 | 109 | // MediaType - a disk "Media" 110 | type MediaType int 111 | 112 | const ( 113 | // UnknownMedia - indicates an unknown media 114 | UnknownMedia MediaType = iota 115 | 116 | // HDD - Spinning hard disk. 117 | HDD 118 | 119 | // SSD - Solid State Disk 120 | SSD 121 | ) 122 | 123 | func (t MediaType) String() string { 124 | return []string{"UNKNOWN", "HDD", "SSD"}[t] 125 | } 126 | 127 | // MarshalJSON for string output rather than int 128 | func (t MediaType) MarshalJSON() ([]byte, error) { 129 | return json.Marshal(t.String()) 130 | } 131 | 132 | // MegaRaid - basic interface 133 | type MegaRaid interface { 134 | // Query - Query the controller provided 135 | Query(int) (Controller, error) 136 | 137 | // GetDiskType - Determine the disk type if controller owns disk 138 | GetDiskType(string) (disko.DiskType, error) 139 | 140 | // DriverSysfsPath - Return the sysfs path to the linux driver for this controller 141 | DriverSysfsPath() string 142 | 143 | // IsSysPathRAID - Check if sysfs path is a device on the controller 144 | IsSysPathRAID(string) bool 145 | } 146 | 147 | // ErrNoController - Error reported by Query if no controller is found. 148 | var ErrNoController = errors.New("megaraid Controller not found") 149 | 150 | // ErrUnsupported - Error reported by Query if controller is not supported. 151 | var ErrUnsupported = errors.New("megaraid Controller unsupported") 152 | 153 | // ErrNoStorcli - Error reported by Query if no storcli binary in PATH 154 | var ErrNoStorcli = errors.New("no 'storcli' command in PATH") 155 | -------------------------------------------------------------------------------- /linux/lvmdump.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | func readReportUint64(s string) uint64 { 11 | // lvm --report-format=json --unit=B puts unit 'B' at end of all sizes. 12 | s = strings.TrimSuffix(s, "B") 13 | 14 | num, err := strconv.ParseUint(s, 10, 64) 15 | if err != nil { 16 | panic(fmt.Sprintf("Failed to convert string %s to int64: %s", s, err)) 17 | } 18 | 19 | return num 20 | } 21 | 22 | type lvmPVData struct { 23 | Path string 24 | Size uint64 25 | VGName string 26 | UUID string 27 | Free uint64 28 | MetadataSize uint64 29 | raw map[string]string 30 | } 31 | 32 | func (d *lvmPVData) UnmarshalJSON(b []byte) error { 33 | var m map[string]string 34 | err := json.Unmarshal(b, &m) 35 | 36 | if err != nil { 37 | return err 38 | } 39 | 40 | d.raw = m 41 | d.Path = m["pv_name"] 42 | d.VGName = m["vg_name"] 43 | d.UUID = m["pv_uuid"] 44 | d.Size = readReportUint64(m["pv_size"]) 45 | d.MetadataSize = readReportUint64(m["pv_mda_size"]) 46 | d.Free = readReportUint64(m["pv_free"]) 47 | 48 | return nil 49 | } 50 | 51 | func parsePvReport(report []byte) ([]lvmPVData, error) { 52 | var d map[string]([]map[string]([]lvmPVData)) 53 | err := json.Unmarshal(report, &d) 54 | 55 | if err != nil { 56 | return []lvmPVData{}, err 57 | } 58 | 59 | return d["report"][0]["pv"], nil 60 | } 61 | 62 | func getPvReport(args ...string) ([]lvmPVData, error) { 63 | cmd := []string{"lvm", "pvs", "--options=pv_all,vg_name", "--report-format=json", "--unit=B"} 64 | cmd = append(cmd, args...) 65 | out, stderr, rc := runCommandWithOutputErrorRc(cmd...) 66 | 67 | if rc != 0 { 68 | return []lvmPVData{}, 69 | fmt.Errorf("failed lvm pvs [%d]: %s\n%s", rc, out, stderr) 70 | } 71 | 72 | return parsePvReport(out) 73 | } 74 | 75 | type lvmVGData struct { 76 | Name string 77 | Size uint64 78 | UUID string 79 | Free uint64 80 | raw map[string]string 81 | } 82 | 83 | func (d *lvmVGData) UnmarshalJSON(b []byte) error { 84 | var m map[string]string 85 | err := json.Unmarshal(b, &m) 86 | 87 | if err != nil { 88 | return err 89 | } 90 | 91 | d.raw = m 92 | d.Name = m["vg_name"] 93 | d.Size = readReportUint64(m["vg_size"]) 94 | d.UUID = m["vg_uuid"] 95 | d.Free = readReportUint64(m["vg_free"]) 96 | 97 | return nil 98 | } 99 | 100 | func parseVgReport(report []byte) ([]lvmVGData, error) { 101 | var d map[string]([]map[string]([]lvmVGData)) 102 | err := json.Unmarshal(report, &d) 103 | 104 | if err != nil { 105 | return []lvmVGData{}, err 106 | } 107 | 108 | return d["report"][0]["vg"], nil 109 | } 110 | 111 | func getVgReport(args ...string) ([]lvmVGData, error) { 112 | cmd := []string{"lvm", "vgs", "--options=vg_all", "--report-format=json", "--unit=B"} 113 | cmd = append(cmd, args...) 114 | out, stderr, rc := runCommandWithOutputErrorRc(cmd...) 115 | 116 | if rc != 0 { 117 | return []lvmVGData{}, 118 | fmt.Errorf("failed lvm vgs [%d]: %s\n%s", rc, out, stderr) 119 | } 120 | 121 | return parseVgReport(out) 122 | } 123 | 124 | type lvmLVData struct { 125 | Name string 126 | VGName string 127 | Path string 128 | Size uint64 129 | UUID string 130 | Active bool 131 | Pool string 132 | raw map[string]string 133 | } 134 | 135 | func (d *lvmLVData) UnmarshalJSON(b []byte) error { 136 | var m map[string]string 137 | 138 | err := json.Unmarshal(b, &m) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | d.raw = m 144 | d.Path = m["lv_path"] 145 | d.Name = m["lv_name"] 146 | d.VGName = m["vg_name"] 147 | d.Active = m["lv_active"] == "active" 148 | d.Pool = m["pool_lv"] 149 | d.UUID = m["lv_uuid"] 150 | d.Size = readReportUint64(m["lv_size"]) 151 | 152 | return nil 153 | } 154 | 155 | func parseLvReport(report []byte) ([]lvmLVData, error) { 156 | var d map[string]([]map[string]([]lvmLVData)) 157 | 158 | err := json.Unmarshal(report, &d) 159 | if err != nil { 160 | return []lvmLVData{}, err 161 | } 162 | 163 | return d["report"][0]["lv"], nil 164 | } 165 | 166 | func getLvReport(args ...string) ([]lvmLVData, error) { 167 | cmd := []string{"lvm", "lvs", "--options=lv_all,vg_name", "--report-format=json", "--unit=B"} 168 | cmd = append(cmd, args...) 169 | out, stderr, rc := runCommandWithOutputErrorRc(cmd...) 170 | 171 | if rc != 0 { 172 | return []lvmLVData{}, 173 | fmt.Errorf("failed lvm lvs [%d]: %s\n%s", rc, out, stderr) 174 | } 175 | 176 | return parseLvReport(out) 177 | } 178 | -------------------------------------------------------------------------------- /demo/disk.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | 8 | "github.com/urfave/cli/v2" 9 | "machinerun.io/disko" 10 | "machinerun.io/disko/linux" 11 | "machinerun.io/disko/partid" 12 | ) 13 | 14 | //nolint:gochecknoglobals 15 | var diskCommands = cli.Command{ 16 | Name: "disk", 17 | Usage: "disk / partition commands", 18 | Subcommands: []*cli.Command{ 19 | { 20 | Name: "new-part", 21 | Usage: "Create a new gpt partition and table", 22 | Action: diskNewPartition, 23 | }, 24 | { 25 | Name: "dump", 26 | Usage: "Scan disks on the system and dump data (json)", 27 | Action: diskScan, 28 | }, 29 | { 30 | Name: "show", 31 | Usage: "Scan disks on the system and dump data (human)", 32 | Action: diskShow, 33 | }, 34 | { 35 | Name: "wipe", 36 | Usage: ("Quickly wipe disks on the system. Zero any existing " + 37 | "beginning and end of disk and any existing partitions"), 38 | Action: diskWipe, 39 | }, 40 | }, 41 | } 42 | 43 | func diskScan(c *cli.Context) error { 44 | var err error 45 | var jbytes []byte 46 | 47 | mysys := linux.System() 48 | matchAll := func(d disko.Disk) bool { 49 | return true 50 | } 51 | 52 | if c.Args().Len() == 1 { 53 | // a single argument will only output 1 disk, not an array of one disk. 54 | disk, err := mysys.ScanDisk(c.Args().First()) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | if jbytes, err = json.MarshalIndent(&disk, "", " "); err != nil { 60 | return err 61 | } 62 | 63 | fmt.Printf("%s\n", string(jbytes)) 64 | 65 | return nil 66 | } 67 | 68 | var disks disko.DiskSet 69 | if c.Args().Len() == 0 { 70 | disks, err = mysys.ScanAllDisks(matchAll) 71 | } else { 72 | disks, err = mysys.ScanDisks(matchAll, c.Args().Slice()...) 73 | } 74 | 75 | if err != nil { 76 | return err 77 | } 78 | 79 | if jbytes, err = json.MarshalIndent(disks, "", " "); err != nil { 80 | return err 81 | } 82 | 83 | fmt.Printf("%s\n", string(jbytes)) 84 | 85 | return nil 86 | } 87 | 88 | func diskShow(c *cli.Context) error { 89 | mysys := linux.System() 90 | disks, err := getDiskSet(mysys, c.Args().Slice()...) 91 | 92 | if err != nil { 93 | return err 94 | } 95 | 96 | oDisks := []string{} 97 | for _, d := range disks { 98 | oDisks = append(oDisks, d.Name) 99 | } 100 | 101 | sort.Strings(oDisks) 102 | 103 | for _, n := range oDisks { 104 | d := disks[n] 105 | fmt.Printf("%s\n%s\n", d.String(), d.Details()) 106 | } 107 | 108 | return nil 109 | } 110 | 111 | func getDiskSet(mysys disko.System, paths ...string) (disko.DiskSet, error) { 112 | matchAll := func(d disko.Disk) bool { 113 | return true 114 | } 115 | 116 | return getDiskSetFilter(mysys, matchAll, paths...) 117 | } 118 | 119 | func getDiskSetFilter(mysys disko.System, matcher disko.DiskFilter, paths ...string) (disko.DiskSet, error) { 120 | if len(paths) == 0 || (len(paths) == 1 && paths[0] == "all") { 121 | return mysys.ScanAllDisks(matcher) 122 | } 123 | 124 | return mysys.ScanDisks(matcher, paths...) 125 | } 126 | 127 | func diskWipe(c *cli.Context) error { 128 | mysys := linux.System() 129 | 130 | // only match read-write disks here. 131 | disks, err := getDiskSetFilter( 132 | mysys, 133 | func(d disko.Disk) bool { return !d.ReadOnly }, 134 | c.Args().Slice()...) 135 | 136 | if err != nil { 137 | return err 138 | } 139 | 140 | for _, d := range disks { 141 | if err = mysys.Wipe(d); err != nil { 142 | return err 143 | } 144 | } 145 | 146 | return nil 147 | } 148 | 149 | func diskNewPartition(c *cli.Context) error { 150 | mysys := linux.System() 151 | fname := c.Args().First() 152 | 153 | if fname == "" { 154 | return fmt.Errorf("must provide disk/file to partition") 155 | } 156 | 157 | disk, err := mysys.ScanDisk(fname) 158 | 159 | if err != nil { 160 | return fmt.Errorf("failed to scan %s: %s", fname, err) 161 | } 162 | 163 | fs := disk.FreeSpaces() 164 | if len(fs) != 1 { 165 | return fmt.Errorf("expected 1 free space, found %d", fs) 166 | } 167 | 168 | myGUID := disko.GenGUID() 169 | 170 | part := disko.Partition{ 171 | Start: fs[0].Start, 172 | Last: fs[0].Last, 173 | Type: partid.LinuxLVM, 174 | Name: "smoser1", 175 | ID: myGUID, 176 | Number: uint(1), 177 | } 178 | 179 | if err := mysys.CreatePartition(disk, part); err != nil { 180 | return err 181 | } 182 | 183 | disk, err = mysys.ScanDisk(fname) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | fmt.Printf("%s\n", disk.Details()) 189 | 190 | return nil 191 | } 192 | -------------------------------------------------------------------------------- /mockos/system_test.go: -------------------------------------------------------------------------------- 1 | package mockos_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/smartystreets/goconvey/convey" 7 | "machinerun.io/disko" 8 | "machinerun.io/disko/mockos" 9 | "machinerun.io/disko/partid" 10 | ) 11 | 12 | //nolint:funlen 13 | func TestSystem(t *testing.T) { 14 | myID, _ := disko.StringToGUID("01234567-89AB-CDEF-0123-456789ABCDEF") 15 | 16 | Convey("testing System Model", t, func() { 17 | So(func() { mockos.System("unknown") }, ShouldPanic) 18 | 19 | sys := mockos.System("testdata/model_sys.json") 20 | So(sys, ShouldNotBeNil) 21 | 22 | Convey("Calling ScanAllDisks with no filter function should return all the disks", func() { 23 | diskSet, err := sys.ScanAllDisks(func(d disko.Disk) bool { return true }) 24 | So(err, ShouldBeNil) 25 | So(diskSet, ShouldNotBeEmpty) 26 | 27 | // ravchama and gfahimi. ScanAllDisk does not in any case return error. 28 | // Probably it should return error when there is no found disks to be compatible 29 | // with the rest of functionality 30 | }) 31 | 32 | Convey("Calling ScanDisk on dev/sda path should return the disk(s) with similar path ", func() { 33 | disk, err := sys.ScanDisk("/dev/sda") 34 | So(err, ShouldBeNil) 35 | So(disk, ShouldNotBeNil) 36 | So(disk.Name, ShouldEqual, "sda") 37 | }) 38 | 39 | Convey("Calling ScanDisk on path that does not contain any disk should return error ", func() { 40 | _, err := sys.ScanDisk("path/with/no/disk") 41 | So(err, ShouldNotBeNil) 42 | }) 43 | 44 | Convey("Calling ScanDisk on dev/sda path should return the disk(s) with similar path", func() { 45 | disk, err := sys.ScanDisk("/dev/sda") 46 | So(err, ShouldBeNil) 47 | So(disk, ShouldNotBeNil) 48 | So(disk.Name, ShouldEqual, "sda") 49 | }) 50 | 51 | Convey("Calling ScanDisks with a specific filter on dev/sda path should return corresponding disks", func() { 52 | disk, err := sys.ScanDisks(func(d disko.Disk) bool { return d.Size > 10000 }, "/dev/sda") 53 | So(err, ShouldBeNil) 54 | So(disk, ShouldNotBeNil) 55 | }) 56 | 57 | Convey("Calling ScanDisks with a specific filter on invalid path should return error", func() { 58 | disk, err := sys.ScanDisks(func(d disko.Disk) bool { return d.Size > 10000 }, 59 | "/dev/sda", "path/with/no/disk") 60 | So(err, ShouldNotBeNil) 61 | So(disk, ShouldBeNil) 62 | }) 63 | 64 | Convey("Calling CreatePartition should create a partition in disk", func() { 65 | disk := disko.Disk{ 66 | Name: "sda", 67 | } 68 | partition := disko.Partition{ 69 | Start: 0, 70 | Last: 10000, 71 | ID: myID, 72 | Type: partid.LinuxFS, 73 | Name: "sda1", 74 | Number: 1, 75 | } 76 | 77 | // CreatePartition should probably only get the name 78 | err := sys.CreatePartition(disk, partition) 79 | So(err, ShouldBeNil) 80 | 81 | d, _ := sys.ScanDisk("/dev/sda") 82 | So(len(d.Partitions), ShouldEqual, 1) 83 | _, ok := d.Partitions[1] 84 | So(ok, ShouldBeTrue) 85 | 86 | Convey("Calling DeletePartition should delete the partition with the specific number from a disk", func() { 87 | disk := disko.Disk{ 88 | Name: "sda", 89 | } 90 | 91 | // DeletePartition should probably only get the name 92 | err := sys.DeletePartition(disk, 1) 93 | So(err, ShouldBeNil) 94 | 95 | err = sys.DeletePartition(disk, 10) 96 | So(err, ShouldBeError) 97 | 98 | d, _ := sys.ScanDisk("/dev/sda") 99 | So(len(d.Partitions), ShouldEqual, 0) 100 | 101 | disk.Name = "crap" 102 | err = sys.DeletePartition(disk, 1) 103 | So(err, ShouldBeError) 104 | }) 105 | 106 | Convey("Calling CreatePartition with an existing partition should return error", func() { 107 | err := sys.CreatePartition(disk, partition) 108 | So(err, ShouldNotBeNil) 109 | }) 110 | }) 111 | 112 | Convey("Calling CreatePartitions should create multiple partitions", func() { 113 | disk := disko.Disk{ 114 | Name: "sda", 115 | } 116 | pSet := disko.PartitionSet{ 117 | 1: disko.Partition{ 118 | Start: 0, 119 | Last: 10000 - 1, 120 | ID: myID, 121 | Type: partid.LinuxFS, 122 | Name: "sda1", 123 | Number: 1, 124 | }, 125 | 2: disko.Partition{ 126 | Start: 10000, 127 | Last: 20000 - 1, 128 | ID: myID, 129 | Type: partid.LinuxFS, 130 | Name: "sda2", 131 | Number: 2, 132 | }} 133 | 134 | // CreatePartition should probably only get the name 135 | err := sys.CreatePartitions(disk, pSet) 136 | So(err, ShouldBeNil) 137 | 138 | d, _ := sys.ScanDisk("/dev/sda") 139 | So(len(d.Partitions), ShouldEqual, len(pSet)) 140 | _, ok := d.Partitions[1] 141 | So(ok, ShouldBeTrue) 142 | 143 | _, ok = d.Partitions[2] 144 | So(ok, ShouldBeTrue) 145 | 146 | Convey("Calling DeletePartition should delete the partition with the specific number from a disk", func() { 147 | disk := disko.Disk{ 148 | Name: "sda", 149 | } 150 | 151 | // DeletePartition should probably only get the name 152 | err := sys.DeletePartition(disk, 1) 153 | So(err, ShouldBeNil) 154 | 155 | err = sys.DeletePartition(disk, 2) 156 | So(err, ShouldBeNil) 157 | 158 | err = sys.DeletePartition(disk, 10) 159 | So(err, ShouldBeError) 160 | 161 | d, _ := sys.ScanDisk("/dev/sda") 162 | So(len(d.Partitions), ShouldEqual, 0) 163 | 164 | disk.Name = "crap" 165 | err = sys.DeletePartition(disk, 1) 166 | So(err, ShouldBeError) 167 | }) 168 | }) 169 | 170 | Convey("Calling CreatePartition on a disk not being track by system should return error", func() { 171 | disk := disko.Disk{ 172 | Name: "invalid", 173 | } 174 | partition := disko.Partition{ 175 | Start: 0, 176 | Last: 10000, 177 | ID: myID, 178 | Type: partid.LinuxFS, 179 | Name: "partition1", 180 | Number: 1, 181 | } 182 | err := sys.CreatePartition(disk, partition) 183 | So(err, ShouldNotBeNil) 184 | }) 185 | }) 186 | } 187 | -------------------------------------------------------------------------------- /linux/util_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "machinerun.io/disko" 9 | ) 10 | 11 | func TestParseUdevInfo(t *testing.T) { 12 | data := []byte(`P: /devices/virtual/block/dm-0 13 | N: dm-0 14 | M: dm-0 15 | S: disk/by-id/dm-name-nvme0n1p6_crypt 16 | S: disk/by-id/dm-uuid-CRYPT-LUKS1-b174c64e7a714359a8b56b79fb66e92b-nvme0n1p6_crypt 17 | S: disk/by-uuid/25df9069-80c7-46f4-a47c-305613c2cb6b 18 | S: mapper/nvme0n1p6_crypt 19 | E: DEVLINKS=/dev/disk/by-id/dm-uuid-CRYPT-LUKS1-b174b-nvme0n1p6_crypt ` + 20 | `/dev/mapper/nvme0n1p6_crypt /dev/disk/by-id/dm-name-nvme0n1p6_crypt 21 | E: DEVNAME=/dev/dm-0 22 | `) 23 | 24 | ast := assert.New(t) 25 | 26 | myInfo := disko.UdevInfo{} 27 | ast.Nil(parseUdevInfo(data, &myInfo)) 28 | 29 | ast.Equal( 30 | disko.UdevInfo{ 31 | Name: "dm-0", 32 | SysPath: "/devices/virtual/block/dm-0", 33 | Symlinks: []string{ 34 | "disk/by-id/dm-name-nvme0n1p6_crypt", 35 | 36 | "disk/by-id/dm-uuid-CRYPT-LUKS1-b174c64e7a714359a8b56b79fb66e92b-nvme0n1p6_crypt", 37 | "disk/by-uuid/25df9069-80c7-46f4-a47c-305613c2cb6b", 38 | "mapper/nvme0n1p6_crypt", 39 | }, 40 | Properties: map[string]string{ 41 | "DEVLINKS": ("/dev/disk/by-id/dm-uuid-CRYPT-LUKS1-b174b-nvme0n1p6_crypt /dev/mapper/nvme0n1p6_crypt " + 42 | "/dev/disk/by-id/dm-name-nvme0n1p6_crypt"), 43 | "DEVNAME": "/dev/dm-0", 44 | }, 45 | }, 46 | myInfo) 47 | } 48 | 49 | func TestParseUdevInfo2(t *testing.T) { 50 | data := []byte(`P: /devices/pci0000:00/..../block/sda 51 | N: sda 52 | M: sda 53 | S: disk/by-id/scsi-35000c500a0d8963f 54 | S: disk/by-id/wwn-0x5000c500a0d8963f 55 | S: disk/by-path/pci-0000:05:00.0-scsi-0:0:8:0 56 | E: DEVLINKS=/dev/disk/by-path/pci-0000:05:00.0-scsi-0:0:8:0 57 | E: DEVNAME=/dev/sda 58 | E: DEVTYPE=disk 59 | E: ID_BUS=scsi 60 | E: ID_MODEL=ST1000NX0453 61 | E: ID_MODEL_ENC=ST\x2f1000NX0453\x20\x20\x20 62 | E: ID_VENDOR_ENC=SEAGATE\x20 63 | E: ID_WWN=0x5000c500a0d8963f 64 | E: MAJOR=8 65 | E: MINOR=0 66 | E: SUBSYSTEM=block 67 | E: TAGS=:systemd: 68 | E: USEC_INITIALIZED=1926114 69 | `) 70 | ast := assert.New(t) 71 | myInfo := disko.UdevInfo{} 72 | err := parseUdevInfo(data, &myInfo) 73 | ast.Equal(nil, err) 74 | ast.Equal( 75 | disko.UdevInfo{ 76 | Name: "sda", 77 | SysPath: "/devices/pci0000:00/..../block/sda", 78 | Symlinks: []string{ 79 | "disk/by-id/scsi-35000c500a0d8963f", 80 | 81 | "disk/by-id/wwn-0x5000c500a0d8963f", 82 | "disk/by-path/pci-0000:05:00.0-scsi-0:0:8:0", 83 | }, 84 | Properties: map[string]string{ 85 | "DEVLINKS": "/dev/disk/by-path/pci-0000:05:00.0-scsi-0:0:8:0", 86 | "DEVNAME": "/dev/sda", 87 | "DEVTYPE": "disk", 88 | "ID_BUS": "scsi", 89 | "ID_MODEL": "ST1000NX0453", 90 | "ID_MODEL_ENC": "ST/1000NX0453", 91 | "ID_VENDOR_ENC": "SEAGATE", 92 | "ID_WWN": "0x5000c500a0d8963f", 93 | "MAJOR": "8", 94 | "MINOR": "0", 95 | "SUBSYSTEM": "block", 96 | "TAGS": ":systemd:", 97 | "USEC_INITIALIZED": "1926114", 98 | }, 99 | }, 100 | myInfo) 101 | } 102 | 103 | func TestRunCommandWithOutputErrorRc(t *testing.T) { 104 | assert := assert.New(t) 105 | out, err, rc := runCommandWithOutputErrorRc( 106 | "sh", "-c", "echo -n STDOUT; echo STDERR 1>&2; exit 99") 107 | assert.Equal(out, []byte("STDOUT")) 108 | assert.Equal(err, []byte("STDERR\n")) 109 | assert.Equal(rc, 99) 110 | } 111 | 112 | func TestRunCommandWithOutputErrorRcStdin(t *testing.T) { 113 | assert := assert.New(t) 114 | out, err, rc := runCommandWithOutputErrorRcStdin( 115 | "line1\nline2\n0\n", 116 | "sh", "-c", 117 | `read o; echo "$o"; read o; echo "$o" 1>&2; read rc; exit $rc`) 118 | assert.Equal(out, []byte("line1\n")) 119 | assert.Equal(err, []byte("line2\n")) 120 | assert.Equal(rc, 0) 121 | } 122 | 123 | func TestRunCommandWithStdin(t *testing.T) { 124 | assert := assert.New(t) 125 | assert.Nil(runCommandStdin("the-stdin", "sh", "-c", "exit 0")) 126 | assert.NotNil(runCommandStdin("", "sh", "-c", "exit 1")) 127 | } 128 | 129 | func TestRunCommand(t *testing.T) { 130 | assert := assert.New(t) 131 | assert.Nil(runCommand("sh", "-c", "exit 0")) 132 | assert.NotNil(runCommand("sh", "-c", "exit 1")) 133 | } 134 | 135 | func TestCeilingUp(t *testing.T) { 136 | assert := assert.New(t) 137 | assert.Equal(uint64(100), Ceiling(98, 4)) 138 | } 139 | 140 | func TestCeilingEven(t *testing.T) { 141 | assert := assert.New(t) 142 | assert.Equal(uint64(100), Ceiling(100, 4)) 143 | assert.Equal(uint64(97), Ceiling(97, 1)) 144 | } 145 | 146 | func TestFloorDown(t *testing.T) { 147 | assert := assert.New(t) 148 | assert.Equal(uint64(96), Floor(98, 4)) 149 | } 150 | 151 | func TestFloorEven(t *testing.T) { 152 | assert := assert.New(t) 153 | assert.Equal(uint64(100), Floor(100, 4)) 154 | assert.Equal(uint64(97), Floor(97, 1)) 155 | } 156 | 157 | func TestGetFileSize(t *testing.T) { 158 | data := "This is my data in the file" 159 | 160 | fp, err := os.CreateTemp("", "testSize") 161 | defer os.Remove(fp.Name()) 162 | 163 | if err != nil { 164 | t.Fatalf("Failed to make test file: %s", err) 165 | } 166 | 167 | if _, err := fp.WriteString(data); err != nil { 168 | t.Fatalf("failed writing to file %s: %s", fp.Name(), err) 169 | } 170 | 171 | if err := fp.Sync(); err != nil { 172 | t.Fatal("failed sync") 173 | } 174 | 175 | found, err := getFileSize(fp) 176 | if err != nil { 177 | t.Errorf("Failed to getFileSize: %s", err) 178 | } 179 | 180 | if found != uint64(len(data)) { 181 | t.Errorf("Found size %d expected %d", found, len(data)) 182 | } 183 | } 184 | 185 | func TestLvPath(t *testing.T) { 186 | tables := []struct{ vgName, lvName, expected string }{ 187 | {"vg0", "lv0", "/dev/vg0/lv0"}, 188 | {"vg0", "my-foo_bar", "/dev/vg0/my-foo_bar"}, 189 | } 190 | 191 | for _, table := range tables { 192 | found := lvPath(table.vgName, table.lvName) 193 | if found != table.expected { 194 | t.Errorf("lvPath(%s, %s) returned '%s'. expected '%s'", 195 | table.vgName, table.lvName, found, table.expected) 196 | } 197 | } 198 | } 199 | 200 | func TestVgLv(t *testing.T) { 201 | tables := []struct{ vgName, lvName, expected string }{ 202 | {"vg0", "lv0", "vg0/lv0"}, 203 | {"vg0", "my-foo_bar", "vg0/my-foo_bar"}, 204 | } 205 | 206 | for _, table := range tables { 207 | found := vgLv(table.vgName, table.lvName) 208 | if found != table.expected { 209 | t.Errorf("vgLv(%s, %s) returned '%s'. expected '%s'", 210 | table.vgName, table.lvName, found, table.expected) 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /partid/partid.go: -------------------------------------------------------------------------------- 1 | package partid 2 | 3 | import "fmt" 4 | 5 | //nolint:gochecknoglobals,lll 6 | var ( 7 | // Empty - Unused / Empty partition 8 | Empty = [16]byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} 9 | 10 | // LUKS - Linux LUKS (CA7D7CCB-63ED-4C53-861C-1742536059CC) 11 | LUKS = [16]byte{0xcb, 0x7c, 0x7d, 0xca, 0xed, 0x63, 0x53, 0x4c, 0x86, 0x1c, 0x17, 0x42, 0x53, 0x60, 0x59, 0xcc} 12 | 13 | // LinuxFS - Linux generic filesytem data (0FC63DAF-8483-4772-8E79-3D69D8477DE4) 14 | LinuxFS = [16]byte{0xaf, 0x3d, 0xc6, 0xf, 0x83, 0x84, 0x72, 0x47, 0x8e, 0x79, 0x3d, 0x69, 0xd8, 0x47, 0x7d, 0xe4} 15 | 16 | // LinuxRAID - Linux RAID data (A19D880F-05FC-4D3B-A006-743F0F84911E) 17 | LinuxRAID = [16]byte{0xf, 0x88, 0x9d, 0xa1, 0xfc, 0x5, 0x3b, 0x4d, 0xa0, 0x6, 0x74, 0x3f, 0xf, 0x84, 0x91, 0x1e} 18 | 19 | // LinuxRootX86 - Linux ia32 rootfs (44479540-F297-41B2-9AF7-D131D5F0458A) 20 | LinuxRootX86 = [16]byte{0x40, 0x95, 0x47, 0x44, 0x97, 0xf2, 0xb2, 0x41, 0x9a, 0xf7, 0xd1, 0x31, 0xd5, 0xf0, 0x45, 0x8a} 21 | 22 | // LinuxRootArm32 - Linux arm32 rootfs (69DAD710-2CE4-4E3C-B16C-21A1D49ABED3) 23 | LinuxRootArm32 = [16]byte{0x10, 0xd7, 0xda, 0x69, 0xe4, 0x2c, 0x3c, 0x4e, 0xb1, 0x6c, 0x21, 0xa1, 0xd4, 0x9a, 0xbe, 0xd3} 24 | 25 | // LinuxRootX86_64 - Linux x86_64 rootfs (4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709) 26 | LinuxRootX86_64 = [16]byte{0xe3, 0xbc, 0x68, 0x4f, 0xcd, 0xe8, 0xb1, 0x4d, 0x96, 0xe7, 0xfb, 0xca, 0xf9, 0x84, 0xb7, 0x9} 27 | 28 | // LinuxRootArm64 - Linux/ arm64 rootfs (B921B045-1DF0-41C3-AF44-4C6F280D3FAE) 29 | LinuxRootArm64 = [16]byte{0x45, 0xb0, 0x21, 0xb9, 0xf0, 0x1d, 0xc3, 0x41, 0xaf, 0x44, 0x4c, 0x6f, 0x28, 0xd, 0x3f, 0xae} 30 | 31 | // LinuxDMCrypt - Linux dm-crypt data (7FFEC5C9-2D00-49B7-8941-3EA10A5586B7) 32 | LinuxDMCrypt = [16]byte{0xc9, 0xc5, 0xfe, 0x7f, 0x0, 0x2d, 0xb7, 0x49, 0x89, 0x41, 0x3e, 0xa1, 0xa, 0x55, 0x86, 0xb7} 33 | 34 | // LinuxReserved - Linux Reserved (8DA63339-0007-60C0-C436-083AC8230908) 35 | LinuxReserved = [16]byte{0x39, 0x33, 0xa6, 0x8d, 0x7, 0x0, 0xc0, 0x60, 0xc4, 0x36, 0x8, 0x3a, 0xc8, 0x23, 0x9, 0x8} 36 | 37 | // LinuxLVM - Linux LVM data (E6D6D379-F507-44C2-A23C-238F2A3DF928) 38 | LinuxLVM = [16]byte{0x79, 0xd3, 0xd6, 0xe6, 0x7, 0xf5, 0xc2, 0x44, 0xa2, 0x3c, 0x23, 0x8f, 0x2a, 0x3d, 0xf9, 0x28} 39 | 40 | // LinuxBoot - Linux /boot fs (BC13C2FF-59E6-4262-A352-B275FD6F7172) 41 | LinuxBoot = [16]byte{0xff, 0xc2, 0x13, 0xbc, 0xe6, 0x59, 0x62, 0x42, 0xa3, 0x52, 0xb2, 0x75, 0xfd, 0x6f, 0x71, 0x72} 42 | 43 | // LinuxSwap - Linux swap data (0657FD6D-A4AB-43C4-84E5-0933C84B4F4F) 44 | LinuxSwap = [16]byte{0x6d, 0xfd, 0x57, 0x6, 0xab, 0xa4, 0xc4, 0x43, 0x84, 0xe5, 0x9, 0x33, 0xc8, 0x4b, 0x4f, 0x4f} 45 | 46 | // LinuxHome - Linux /home fs (933AC7E1-2EB4-4F13-B844-0E14E2AEF915) 47 | LinuxHome = [16]byte{0xe1, 0xc7, 0x3a, 0x93, 0xb4, 0x2e, 0x13, 0x4f, 0xb8, 0x44, 0xe, 0x14, 0xe2, 0xae, 0xf9, 0x15} 48 | 49 | // LinuxSrv - Linux /srv fs (3B8F8425-20E0-4F3B-907F-1A25A76F98E8) 50 | LinuxSrv = [16]byte{0x25, 0x84, 0x8f, 0x3b, 0xe0, 0x20, 0x3b, 0x4f, 0x90, 0x7f, 0x1a, 0x25, 0xa7, 0x6f, 0x98, 0xe8} 51 | 52 | // MBR - MBR type (024DEE41-33E7-11D3-9D69-0008C781F39F) 53 | MBR = [16]byte{0x41, 0xee, 0x4d, 0x2, 0xe7, 0x33, 0xd3, 0x11, 0x9d, 0x69, 0x0, 0x8, 0xc7, 0x81, 0xf3, 0x9f} 54 | 55 | // BiosBoot - Bios Boot Partition (BBP) (21686148-6449-6E6F-744E-656564454649) 56 | BiosBoot = [16]byte{0x48, 0x61, 0x68, 0x21, 0x49, 0x64, 0x6f, 0x6e, 0x74, 0x4e, 0x65, 0x65, 0x64, 0x45, 0x46, 0x49} 57 | 58 | // EFI - EFI Partition (C12A7328-F81F-11D2-BA4B-00A0C93EC93B) 59 | EFI = [16]byte{0x28, 0x73, 0x2a, 0xc1, 0x1f, 0xf8, 0xd2, 0x11, 0xba, 0x4b, 0x0, 0xa0, 0xc9, 0x3e, 0xc9, 0x3b} 60 | 61 | // AtxReserved - F79962B9-24E6-9948-9F94-E6BFDAD2771F 62 | AtxReserved = [16]byte{0xb9, 0x62, 0x99, 0xf7, 0xe6, 0x24, 0x48, 0x99, 0x9f, 0x94, 0xe6, 0xbf, 0xda, 0xd2, 0x77, 0x1f} 63 | 64 | // AtxPBF - 01A3E19F-9FEA-ED47-92C2-E75639FF5601 65 | AtxPBF = [16]byte{0x9f, 0xe1, 0xa3, 0x01, 0xea, 0x9f, 0x47, 0xed, 0x92, 0xc2, 0xe7, 0x56, 0x39, 0xff, 0x56, 0x01} 66 | 67 | // AtxSBF - 01A3E19F-9FEA-ED47-92C2-E75639FF5602 68 | AtxSBF = [16]byte{0x9f, 0xe1, 0xa3, 0x01, 0xea, 0x9f, 0x47, 0xed, 0x92, 0xc2, 0xe7, 0x56, 0x39, 0xff, 0x56, 0x02} 69 | 70 | // AtxSignData - 8DEE18B1-77B5-1D25-AE89-2A252D1A422F 71 | AtxSignData = [16]byte{0xb1, 0x18, 0xee, 0x8d, 0xb5, 0x77, 0x25, 0x1d, 0xae, 0x89, 0x2a, 0x25, 0x2d, 0x1a, 0x42, 0x2f} 72 | 73 | // AtxCargo - 874B4379-38B4-4A43-8DD0-3D6FE8C9BD83 74 | AtxCargo = [16]byte{0x79, 0x43, 0x4B, 0x87, 0xb4, 0x38, 0x43, 0x4a, 0x8d, 0xd0, 0x3d, 0x6f, 0xe8, 0xc9, 0xbd, 0x83} 75 | 76 | // StoragedRaw - 26843217-D7A8-48E8-BBFC-6870C69BA060 77 | StoragedRaw = [16]byte{0x17, 0x32, 0x84, 0x26, 0xa8, 0xd7, 0xe8, 0x48, 0xbb, 0xfc, 0x68, 0x70, 0xc6, 0x9b, 0xa0, 0x60} 78 | 79 | // StoragedLVM - D5842A1E-DF14-4129-94DB-9C06DF842179 80 | StoragedLVM = [16]byte{0x1e, 0x2a, 0x84, 0xd5, 0x14, 0xdf, 0x29, 0x41, 0x94, 0xdb, 0x9C, 0x06, 0xdf, 0x84, 0x21, 0x79} 81 | 82 | // Microsoft Basic Data - EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 83 | MicrosoftBasicData = [16]byte{0xa2, 0xa0, 0xd0, 0xeb, 0xe5, 0xb9, 0x33, 0x44, 0x87, 0xc0, 0x68, 0xb6, 0xb7, 0x26, 0x99, 0xc7} 84 | ) 85 | 86 | // Text gives human readable names 87 | var Text = map[[16]byte]string{ //nolint:gochecknoglobals 88 | EFI: "EFI", 89 | LUKS: "LUKS", 90 | LinuxFS: "Linux-FS", 91 | LinuxRAID: "RAID", 92 | LinuxRootX86: "Linux-root-x86", 93 | LinuxRootArm32: "Linux-root-arm32", 94 | LinuxRootX86_64: "Linux-root-x86_64", 95 | LinuxRootArm64: "Linux-root-arm64", 96 | LinuxDMCrypt: "DM-crypt", 97 | LinuxReserved: "Linux-Reserved", 98 | LinuxLVM: "LVM", 99 | LinuxBoot: "Linux-/boot", 100 | LinuxSwap: "Linux-swap", 101 | LinuxHome: "Linux-/home", 102 | LinuxSrv: "Linux-/srv", 103 | MBR: "MBR", 104 | BiosBoot: "Bios-Boot", 105 | StoragedRaw: "Storaged-Raw", 106 | StoragedLVM: "Storaged-LVM", 107 | AtxCargo: "Atomix-Cargo", 108 | AtxPBF: "Atomix-PBF", 109 | AtxReserved: "Atomix-Reserved", 110 | AtxSBF: "Atomix-SBF", 111 | AtxSignData: "Atomix-SignData", 112 | MicrosoftBasicData: "MS-BasicData", 113 | } 114 | 115 | //nolint:gochecknoglobals,gomnd 116 | var mapGPTToMBR = map[[16]byte]byte{ 117 | Empty: 0x00, 118 | LinuxSwap: 0x82, 119 | LinuxFS: 0x83, 120 | LinuxLVM: 0x8E, 121 | LUKS: 0xE8, 122 | } 123 | 124 | // PartTypeToMBR - Convert a GPT Type to its MBR equivalent 125 | func PartTypeToMBR(gptType [16]byte) (byte, error) { 126 | if val, ok := mapGPTToMBR[gptType]; ok { 127 | return val, nil 128 | } 129 | 130 | padded := true 131 | 132 | for i := 0; i < 15; i++ { 133 | if gptType[i] != 0 { 134 | padded = false 135 | break 136 | } 137 | } 138 | 139 | if padded { 140 | return gptType[15], nil 141 | } 142 | 143 | return 0, fmt.Errorf("unknown MBR type %v", gptType) 144 | } 145 | -------------------------------------------------------------------------------- /linux/system_test.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "machinerun.io/disko" 11 | "machinerun.io/disko/partid" 12 | ) 13 | 14 | func TestGetDiskProperties(t *testing.T) { 15 | azureSys := ("/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/VMBUS:01" + 16 | "/00000000-0001-8899-0000-000000000000/host1/target1:0:1/1:0:1:0/block/sdb") 17 | scsiSys := "/devices/pci0000:00/0000:00:02.2/0000:05:00.0/host0/target0:0:8/0:0:8:0/block/sda" 18 | 19 | tables := []struct { 20 | info disko.UdevInfo 21 | expected disko.PropertySet 22 | }{ 23 | { 24 | disko.UdevInfo{ 25 | Name: "sda", 26 | SysPath: scsiSys, 27 | Symlinks: []string{}, 28 | Properties: map[string]string{ 29 | "ID_MODEL": "SPCC M.2 PCIe SSD", 30 | "ID_REVISION": "ECFM22.6"}}, 31 | disko.PropertySet{disko.Ephemeral: false}}, 32 | { 33 | disko.UdevInfo{ 34 | Name: "sdb", 35 | SysPath: azureSys, 36 | Symlinks: []string{}, 37 | Properties: map[string]string{ 38 | "ID_MODEL": "SPCC M.2 PCIe SSD", 39 | "ID_REVISION": "ECFM22.6"}}, 40 | disko.PropertySet{disko.Ephemeral: true}}, 41 | { 42 | disko.UdevInfo{ 43 | Name: "sdb", 44 | SysPath: azureSys, 45 | Symlinks: []string{}, 46 | Properties: map[string]string{ 47 | "DM_MULTIPATH_DEVICE_PATH": "0", 48 | "ID_SERIAL_SHORT": "AWS628703BD8E5BEB551", 49 | "ID_WWN": "nvme.1d0f-4157...4616e63652053746f72616765-00000001", 50 | "ID_MODEL": "Amazon EC2 NVMe Instance Storage", 51 | "ID_REVISION": "0", 52 | "ID_SERIAL": "Amazon EC2 NVMe Instance Storage_AWS628703BD8E5BEB551"}}, 53 | 54 | disko.PropertySet{disko.Ephemeral: true}}, 55 | } 56 | 57 | for _, table := range tables { 58 | found := getDiskProperties(table.info) 59 | bad := []disko.Property{} 60 | 61 | for k, v := range table.expected { 62 | if found[k] != v { 63 | bad = append(bad, k) 64 | } 65 | } 66 | 67 | for k, v := range found { 68 | if table.expected[k] != v { 69 | bad = append(bad, k) 70 | } 71 | } 72 | 73 | if len(bad) != 0 { 74 | t.Errorf("getDiskProperties(%v) returned '%v'. expected '%v'", 75 | table.info, found, table.expected) 76 | } 77 | } 78 | } 79 | 80 | func genEmptyDisk(tmpd string, fsize uint64) (disko.Disk, error) { 81 | fpath := path.Join(tmpd, "mydisk") 82 | 83 | disk := disko.Disk{ 84 | Name: "mydisk", 85 | Path: fpath, 86 | Size: fsize, 87 | SectorSize: sectorSize512, 88 | } 89 | 90 | if err := os.WriteFile(fpath, []byte{}, 0600); err != nil { 91 | return disk, fmt.Errorf("Failed to write to a temp file: %s", err) 92 | } 93 | 94 | if err := os.Truncate(fpath, int64(fsize)); err != nil { 95 | return disk, fmt.Errorf("Failed create empty file: %s", err) 96 | } 97 | 98 | fs := disk.FreeSpaces() 99 | if len(fs) != 1 { 100 | return disk, fmt.Errorf("Expected 1 free space, found %d", fs) 101 | } 102 | 103 | return disk, nil 104 | } 105 | 106 | func TestCreatePartitionsMBR(t *testing.T) { 107 | ast := assert.New(t) 108 | 109 | tmpd, err := os.MkdirTemp("", "disko_test") 110 | if err != nil { 111 | t.Fatalf("Failed to create tempdir: %s", err) 112 | } 113 | 114 | defer os.RemoveAll(tmpd) 115 | 116 | disk, err := genEmptyDisk(tmpd, 50*disko.Mebibyte) 117 | if err != nil { 118 | t.Fatalf("Creation of temp disk failed: %s", err) 119 | } 120 | 121 | disk.Table = disko.MBR 122 | 123 | part1 := disko.Partition{ 124 | Start: 4 * disko.Mebibyte, 125 | Last: 20*disko.Mebibyte - 1, 126 | Type: partid.LinuxFS, 127 | Name: "ignored-for-mbr", 128 | ID: disko.GenGUID(), 129 | Number: uint(1), 130 | } 131 | 132 | pSet := disko.PartitionSet{1: part1} 133 | 134 | sys := System() 135 | if err := sys.CreatePartitions(disk, pSet); err != nil { 136 | t.Errorf("CreatePartitions failed: %s", err) 137 | } 138 | 139 | fp, err := os.Open(disk.Path) 140 | if err != nil { 141 | t.Fatalf("Failed to open disk image %s: %s", disk.Path, err) 142 | } 143 | 144 | pSetFound, _, _, err := findPartitions(fp) 145 | if err != nil { 146 | t.Fatalf("Failed to findPartitions on %s: %s", disk.Path, err) 147 | } 148 | 149 | if len(pSetFound) != len(pSet) { 150 | t.Errorf("Scanned found %d partitions, expected %d", len(pSetFound), len(pSet)) 151 | } 152 | 153 | mbrTypeLinux := disko.PartType{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x83} 154 | expPart1 := part1 155 | expPart1.Type = mbrTypeLinux 156 | expPart1.Name = "" 157 | expPart1.ID = disko.GUID{} 158 | 159 | scannedDisk, err := sys.ScanDisk(disk.Path) 160 | if err != nil { 161 | t.Errorf("Failed to scan disk-image") 162 | } 163 | 164 | ast.Equal(expPart1, pSetFound[1]) 165 | 166 | ast.Equal(disk.Size, scannedDisk.Size) 167 | ast.Equal(disko.FILESYSTEM, scannedDisk.Attachment) 168 | ast.Equal(disko.TYPEFILE, scannedDisk.Type) 169 | ast.Equal(disko.MBR, scannedDisk.Table) 170 | } 171 | 172 | func TestCreatePartitions(t *testing.T) { 173 | ast := assert.New(t) 174 | 175 | tmpd, err := os.MkdirTemp("", "disko_test") 176 | if err != nil { 177 | t.Fatalf("Failed to create tempdir: %s", err) 178 | } 179 | 180 | defer os.RemoveAll(tmpd) 181 | 182 | disk, err := genEmptyDisk(tmpd, 50*disko.Mebibyte) 183 | if err != nil { 184 | t.Fatalf("Creation of temp disk failed: %s", err) 185 | } 186 | 187 | part1 := disko.Partition{ 188 | Start: 4 * disko.Mebibyte, 189 | Last: 20*disko.Mebibyte - 1, 190 | Type: partid.LinuxHome, 191 | Name: "mytest 1", 192 | ID: disko.GenGUID(), 193 | Number: uint(1), 194 | } 195 | 196 | part2 := disko.Partition{ 197 | Start: 20 * disko.Mebibyte, 198 | Last: 40*disko.Mebibyte - 1, 199 | Type: partid.LinuxFS, 200 | Name: "mytest 2", 201 | ID: disko.GenGUID(), 202 | Number: uint(2), 203 | } 204 | 205 | pSet := disko.PartitionSet{1: part1, 2: part2} 206 | 207 | sys := System() 208 | if err := sys.CreatePartitions(disk, pSet); err != nil { 209 | t.Errorf("CreatePartitions failed: %s", err) 210 | } 211 | 212 | fp, err := os.Open(disk.Path) 213 | if err != nil { 214 | t.Fatalf("Failed to open disk image %s: %s", disk.Path, err) 215 | } 216 | 217 | pSetFound, _, _, err := findPartitions(fp) 218 | if err != nil { 219 | t.Fatalf("Failed to findPartitions on %s: %s", disk.Path, err) 220 | } 221 | 222 | if len(pSetFound) != len(pSet) { 223 | t.Errorf("Scanned found %d partitions, expected %d", len(pSetFound), len(pSet)) 224 | } 225 | 226 | ast.Equal(part1, pSetFound[1]) 227 | ast.Equal(part2, pSetFound[2]) 228 | 229 | scannedDisk, err := sys.ScanDisk(disk.Path) 230 | if err != nil { 231 | t.Errorf("Failed to scan disk-image") 232 | } 233 | 234 | ast.Equal(disk.Size, scannedDisk.Size) 235 | ast.Equal(disko.FILESYSTEM, scannedDisk.Attachment) 236 | ast.Equal(disko.TYPEFILE, scannedDisk.Type) 237 | } 238 | -------------------------------------------------------------------------------- /lvm.go: -------------------------------------------------------------------------------- 1 | package disko 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // VolumeManager provides logical volume oprations that allows for creation and 9 | // management of volume groups, physical volumes and logical volumes. 10 | type VolumeManager interface { 11 | // ScanPVs scans the system for all the PVs and returns the set of PVs that 12 | // are accepted by the filter function. 13 | ScanPVs(filter PVFilter) (PVSet, error) 14 | 15 | // ScanVGs scans the systems for all the VGs and returns the set of VGs that 16 | // are accepted by the filter function. 17 | ScanVGs(filter VGFilter) (VGSet, error) 18 | 19 | // CreatePV creates a PV with specified name. 20 | CreatePV(diskName string) (PV, error) 21 | 22 | // DeletePV deletes the specified PV. 23 | DeletePV(pv PV) error 24 | 25 | // HasPV returns true if the pv exists. This indicates that the device 26 | // already has an lvm pv header. 27 | HasPV(name string) bool 28 | 29 | // CreateVG creates a VG with specified name and adds the provided pvs to 30 | // this vg. 31 | CreateVG(name string, pvs ...PV) (VG, error) 32 | 33 | // ExtendVG extends the volument group storage capacity with the specified 34 | // PVs. 35 | ExtendVG(vgName string, pvs ...PV) error 36 | 37 | // Delete deletes this VG and all the LVs in the VG. 38 | RemoveVG(vgName string) error 39 | 40 | // HasVG returns true if the vg exists. 41 | HasVG(vgName string) bool 42 | 43 | // CryptFormat setups up encryption for this volume using the provided key. 44 | CryptFormat(vgName string, lvName string, key string) error 45 | 46 | // CryptOpen opens the encrypted logical volume for use using the provided 47 | // key. 48 | CryptOpen(vgName string, lvName string, decryptedName string, key string) error 49 | 50 | // CryptClose close the encrypted logical volume using the provided key. 51 | CryptClose(vgName string, lvName string, decryptedName string) error 52 | 53 | // CreateLV creates a LV with specified name, size and type. 54 | CreateLV(vgName string, name string, size uint64, lvType LVType) (LV, error) 55 | 56 | // RemoveLV removes this LV. 57 | RemoveLV(vgName string, lvName string) error 58 | 59 | // RenameLV renames this LV to newLvName. 60 | RenameLV(vgName string, lvName string, newLvName string) error 61 | 62 | // ExtendLV expands the LV to the requested new size. 63 | ExtendLV(vgName string, lvName string, newSize uint64) error 64 | 65 | // HasVG returns true if the lv exists. 66 | HasLV(vgName string, name string) bool 67 | } 68 | 69 | // PV wraps a LVM physical volume. A lvm physical volume is the raw 70 | // block device or other disk like devices that provide storage capacity. 71 | type PV struct { 72 | // Name returns the name of the PV. 73 | Name string `json:"name"` 74 | 75 | // UUID for the PV 76 | UUID string `json:"uuid"` 77 | 78 | // Path returns the device path of the PV. 79 | Path string `json:"path"` 80 | 81 | // Size returns the size of the PV. 82 | Size uint64 `json:"size"` 83 | 84 | // The volume group this PV is part of ("" if none) 85 | VGName string `json:"vgname"` 86 | 87 | // FreeSize returns the free size of the PV. 88 | FreeSize uint64 `json:"freeSize"` 89 | } 90 | 91 | // PVSet is a set of PVs indexed by their names. 92 | type PVSet map[string]PV 93 | 94 | // LVSet is a map of LV names to the LV. 95 | type LVSet map[string]LV 96 | 97 | // ExtentSize is extent size for lvm 98 | const ExtentSize = 4 * Mebibyte 99 | 100 | // LV interface wraps the lvm logical volume information and operations. A 101 | // logical volume partitions a volume group into a slice of capacity that can 102 | // be used a block device to create a file system. 103 | type LV struct { 104 | // Name is the name of the logical volume. 105 | Name string `json:"name"` 106 | 107 | // UUID for the LV 108 | UUID string `json:"uuid"` 109 | 110 | // Path is the full path of the logical volume. 111 | Path string `json:"path"` 112 | 113 | // Size the size of the logical volume. 114 | Size uint64 `json:"size"` 115 | 116 | // Type is the type of logical volume. 117 | Type LVType `json:"type"` 118 | 119 | // The volume group that this logical volume is part of. 120 | VGName string `json:"vgname"` 121 | 122 | // Encrypted indicates if the logical volume is encrypted. 123 | Encrypted bool `json:"encrypted"` 124 | 125 | // DecryptedLVName is the name of the decrypted logical volume as set by 126 | // the CryptOpen call. 127 | DecryptedLVName string `json:"decryptedLVName"` 128 | 129 | // DecryptedLVPath is the full path of the decrypted logical volume. This 130 | // is set only for encrypted volumes, using the CryptFormat. 131 | DecryptedLVPath string `json:"decryptedLVPath"` 132 | } 133 | 134 | // LVType defines the type of the logical volume. 135 | type LVType int 136 | 137 | const ( 138 | // THICK indicates thickly provisioned logical volume. 139 | THICK LVType = iota 140 | 141 | // THIN indicates thinly provisioned logical volume. 142 | THIN 143 | 144 | // THINPOOL indicates a pool lv for other lvs 145 | THINPOOL 146 | 147 | // LVTypeUnknown - unknown type 148 | LVTypeUnknown 149 | ) 150 | 151 | //nolint:gochecknoglobals 152 | var lvTypes = map[string]LVType{ 153 | "THICK": THICK, 154 | "THIN": THIN, 155 | "THINPOOL": THINPOOL, 156 | "UNKNOWN": LVTypeUnknown, 157 | } 158 | 159 | func (t LVType) String() string { 160 | for k, v := range lvTypes { 161 | if v == t { 162 | return k 163 | } 164 | } 165 | 166 | return fmt.Sprintf("UNKNOWN-%d", t) 167 | } 168 | 169 | func stringToLVType(s string) LVType { 170 | if val, ok := lvTypes[s]; ok { 171 | return val 172 | } 173 | 174 | return LVTypeUnknown 175 | } 176 | 177 | // UnmarshalJSON - custom to read as strings or int 178 | func (t *LVType) UnmarshalJSON(b []byte) error { 179 | var err error 180 | var asStr string 181 | var asInt int 182 | 183 | err = json.Unmarshal(b, &asInt) 184 | if err == nil { 185 | *t = LVType(asInt) 186 | return nil 187 | } 188 | 189 | err = json.Unmarshal(b, &asStr) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | lvtype := stringToLVType(asStr) 195 | *t = lvtype 196 | 197 | return nil 198 | } 199 | 200 | // MarshalJSON - serialize to json 201 | func (t LVType) MarshalJSON() ([]byte, error) { 202 | return json.Marshal(t.String()) 203 | } 204 | 205 | // VG wraps a LVM volume group. A volume group combines one or more 206 | // physical volumes into storage pools and provides a unified logical device 207 | // with combined storage capacity of the underlying physical volumes. 208 | type VG struct { 209 | // Name is the name of the volume group. 210 | Name string `json:"name"` 211 | 212 | // UUID for the VG 213 | UUID string `json:"uuid"` 214 | 215 | // Size is the current size of the volume group. 216 | Size uint64 `json:"size"` 217 | 218 | // Volumes is set of all the volumes in this volume group. 219 | Volumes LVSet `json:"volumes"` 220 | 221 | // FreeSpace is the amount free space left in the volume group. 222 | FreeSpace uint64 `json:"freeSpace"` 223 | 224 | // PVs is the set of PVs that belongs to this VG. 225 | PVs PVSet `json:"pvs"` 226 | } 227 | 228 | // VGSet is set of volume groups indexed by their name. 229 | type VGSet map[string]VG 230 | 231 | // Details returns a formatted string with the information of volume groups. 232 | func (vgs VGSet) Details() string { 233 | return "" 234 | } 235 | -------------------------------------------------------------------------------- /linux/root_helpers_test.go: -------------------------------------------------------------------------------- 1 | package linux_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "math/rand" 9 | "os" 10 | "os/exec" 11 | "path" 12 | "strings" 13 | "syscall" 14 | "testing" 15 | "time" 16 | 17 | "golang.org/x/sys/unix" 18 | ) 19 | 20 | type cleanList struct { 21 | cleaners []cleaner 22 | } 23 | 24 | func (c *cleanList) Cleanup(t *testing.T) { 25 | for i := len(c.cleaners) - 1; i >= 0; i-- { 26 | if err := c.cleaners[i].Func(); err != nil { 27 | t.Errorf("cleanup %s: %s", c.cleaners[i].Purpose, err) 28 | } 29 | } 30 | } 31 | 32 | func (c *cleanList) Add(cl cleaner) { 33 | c.cleaners = append(c.cleaners, cl) 34 | } 35 | 36 | func (c *cleanList) AddF(f func() error, msg string) { 37 | c.cleaners = append(c.cleaners, cleaner{f, msg}) 38 | } 39 | 40 | type cleaner struct { 41 | Func func() error 42 | Purpose string 43 | } 44 | 45 | func getCommandErrorRCDefault(err error, rcError int) int { 46 | if err == nil { 47 | return 0 48 | } 49 | 50 | exitError, ok := err.(*exec.ExitError) 51 | if ok { 52 | if status, ok := exitError.Sys().(syscall.WaitStatus); ok { 53 | return status.ExitStatus() 54 | } 55 | } 56 | 57 | return rcError 58 | } 59 | 60 | func getCommandErrorRC(err error) int { 61 | return getCommandErrorRCDefault(err, 127) 62 | } 63 | 64 | func cmdError(args []string, out []byte, err []byte, rc int) error { 65 | if rc == 0 { 66 | return nil 67 | } 68 | 69 | return errors.New(cmdString(args, out, err, rc)) 70 | } 71 | 72 | func cmdString(args []string, out []byte, err []byte, rc int) string { 73 | tlen := len(err) 74 | if tlen == 0 || err[tlen-1] != '\n' { 75 | err = append(err, '\n') 76 | } 77 | 78 | tlen = len(out) 79 | if tlen == 0 || out[tlen-1] != '\n' { 80 | out = append(out, '\n') 81 | } 82 | 83 | return fmt.Sprintf( 84 | "command returned %d:\n cmd: %v\n out: %s err: %s", 85 | rc, args, out, err) 86 | } 87 | 88 | func runCommand(args ...string) error { 89 | out, err, rc := runCommandWithOutputErrorRc(args...) 90 | return cmdError(args, out, err, rc) 91 | } 92 | 93 | func runCommandWithOutputErrorRc(args ...string) ([]byte, []byte, int) { 94 | cmd := exec.Command(args[0], args[1:]...) //nolint:gosec 95 | var stdout, stderr bytes.Buffer 96 | cmd.Stdout = &stdout 97 | cmd.Stderr = &stderr 98 | err := cmd.Run() 99 | 100 | return stdout.Bytes(), stderr.Bytes(), getCommandErrorRC(err) 101 | } 102 | 103 | // connectLoop - connect fname to a loop device. 104 | // 105 | // return cleanup, devicePath, error 106 | func connectLoop(fname string) (func() error, string, error) { 107 | var cmd = []string{"losetup", "--find", "--show", "--partscan", fname} 108 | var stdout, stderr []byte 109 | var rc int 110 | 111 | if stdout, stderr, rc = runCommandWithOutputErrorRc(cmd...); rc != 0 { 112 | return func() error { return nil }, "", cmdError(cmd, stdout, stderr, rc) 113 | } 114 | 115 | // chomp the trailing '\n' 116 | devPath := string(stdout[0 : len(stdout)-1]) 117 | 118 | cleanup := func() error { 119 | return runCommand("losetup", "--detach="+devPath) 120 | } 121 | 122 | return cleanup, devPath, waitForFileSize(devPath) 123 | } 124 | 125 | func waitForFileSize(devPath string) error { 126 | fp, err := os.OpenFile(devPath, os.O_RDWR, 0) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | defer fp.Close() 132 | 133 | diskLen := int64(0) 134 | napLen := time.Millisecond * 10 135 | startTime := time.Now() 136 | endTime := startTime.Add(30 * time.Second) 137 | 138 | for { 139 | if diskLen, err = fp.Seek(0, io.SeekEnd); err != nil { 140 | return err 141 | } else if diskLen != 0 { 142 | return nil 143 | } 144 | 145 | time.Sleep(napLen) 146 | 147 | if time.Now().After(endTime) { 148 | break 149 | } 150 | } 151 | 152 | return fmt.Errorf("gave up waiting after %v for non-zero length in %s", 153 | time.Since(startTime), devPath) 154 | } 155 | 156 | func getTempDir() (cleaner, string) { 157 | p, err := os.MkdirTemp("", "disko_test") 158 | if err != nil { 159 | panic(err) 160 | } 161 | 162 | return cleaner{func() error { return os.RemoveAll(p) }, "remove tmpDir " + p}, p 163 | } 164 | 165 | func getTempFile(size int64) (cleaner, string) { 166 | fp, err := os.CreateTemp("", "disko_test") 167 | if err != nil { 168 | panic(err) 169 | } 170 | 171 | name := fp.Name() 172 | fp.Close() 173 | 174 | if err := os.Truncate(name, size); err != nil { 175 | panic(err) 176 | } 177 | 178 | return cleaner{func() error { return os.Remove(name) }, "remove tempFile " + name}, name 179 | } 180 | 181 | // we don't need crypto/math random numbers to construct a random string 182 | // 183 | //nolint:gosec 184 | func randStr(n int) string { 185 | var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 186 | 187 | b := make([]rune, n) 188 | for i := range b { 189 | b[i] = letters[rand.Intn(len(letters))] 190 | } 191 | 192 | return string(b) 193 | } 194 | 195 | func isRoot() error { 196 | uid := os.Geteuid() 197 | if uid == 0 { 198 | return nil 199 | } 200 | 201 | return fmt.Errorf("not root (euid=%d)", uid) 202 | } 203 | 204 | func writableCharDev(path string) error { 205 | fi, err := os.Stat(path) 206 | if err != nil { 207 | if os.IsNotExist(err) { 208 | return fmt.Errorf("%s: did not exist", path) 209 | } 210 | 211 | return fmt.Errorf("%s: %s", path, err) 212 | } 213 | 214 | if fi.Mode()&os.ModeCharDevice != os.ModeCharDevice { 215 | return fmt.Errorf("%s: not a character device", path) 216 | } 217 | 218 | if err := unix.Access(path, unix.W_OK); err != nil { 219 | return fmt.Errorf("%s: not writable", path) 220 | } 221 | 222 | return nil 223 | } 224 | 225 | func hasCommand(name string) error { 226 | p := which(name) 227 | if p == "" { 228 | return fmt.Errorf("%s: command not present", p) 229 | } 230 | 231 | return nil 232 | } 233 | 234 | func which(name string) string { 235 | return whichSearch(name, strings.Split(os.Getenv("PATH"), ":")) 236 | } 237 | 238 | func whichSearch(name string, paths []string) string { 239 | var search []string 240 | 241 | if strings.ContainsRune(name, os.PathSeparator) { 242 | if path.IsAbs(name) { 243 | search = []string{name} 244 | } else { 245 | search = []string{"./" + name} 246 | } 247 | } else { 248 | search = []string{} 249 | for _, p := range paths { 250 | search = append(search, path.Join(p, name)) 251 | } 252 | } 253 | 254 | for _, fPath := range search { 255 | if err := unix.Access(fPath, unix.X_OK); err == nil { 256 | return fPath 257 | } 258 | } 259 | 260 | return "" 261 | } 262 | 263 | func canUseLoop() error { 264 | if err := writableCharDev("/dev/loop-control"); err != nil { 265 | return err 266 | } 267 | 268 | return hasCommand("losetup") 269 | } 270 | 271 | func canUseLVM() error { 272 | if err := writableCharDev("/dev/mapper/control"); err != nil { 273 | return err 274 | } 275 | 276 | return hasCommand("lvm") 277 | } 278 | 279 | // iSkipOrFail - run checks 280 | func iSkipOrFail(t *testing.T, checks ...func() error) { 281 | const envName = "DISKO_INTEGRATION" 282 | const allowSkip = "allow-skip" 283 | 284 | mode := os.Getenv(envName) 285 | 286 | switch mode { 287 | case "": 288 | mode = allowSkip 289 | case "run", allowSkip: 290 | case "skip": 291 | t.Skip(envName + "=" + mode) 292 | return // be explicit (not actually necessary) 293 | default: 294 | panic("Invalid value for " + envName + ": " + mode) 295 | } 296 | 297 | errors := []error{} 298 | 299 | for _, c := range checks { 300 | if err := c(); err != nil { 301 | if mode == allowSkip { 302 | t.Skip(err) 303 | return 304 | } 305 | 306 | errors = append(errors, err) 307 | } 308 | } 309 | 310 | if len(errors) == 0 { 311 | return 312 | } 313 | 314 | // mode is "run" and there are errors. 315 | for _, err := range errors { 316 | t.Error(err) 317 | } 318 | 319 | t.FailNow() 320 | } 321 | -------------------------------------------------------------------------------- /mockos/lvm_test.go: -------------------------------------------------------------------------------- 1 | package mockos_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | . "github.com/smartystreets/goconvey/convey" 8 | "machinerun.io/disko" 9 | "machinerun.io/disko/mockos" 10 | "machinerun.io/disko/partid" 11 | ) 12 | 13 | func TestPV(t *testing.T) { 14 | Convey("testing lvm PVs", t, func() { 15 | sys := mockos.System("testdata/model_sys.json") 16 | So(sys, ShouldNotBeNil) 17 | 18 | lvm := mockos.LVM(sys) 19 | So(lvm, ShouldNotBeNil) 20 | pvs, err := lvm.ScanPVs(func(f disko.PV) bool { return true }) 21 | So(err, ShouldBeNil) 22 | So(pvs, ShouldBeEmpty) 23 | 24 | _, err = lvm.CreatePV("sdxx") 25 | So(err, ShouldBeError) 26 | 27 | pv, err := lvm.CreatePV("sda") 28 | So(err, ShouldBeNil) 29 | So(pv.Name, ShouldEqual, "sda") 30 | So(lvm.HasPV("sda"), ShouldBeTrue) 31 | 32 | _, err = lvm.CreatePV("sda") 33 | So(err, ShouldBeError) 34 | 35 | err = lvm.DeletePV(disko.PV{Name: "blah"}) 36 | So(err, ShouldBeError) 37 | 38 | err = lvm.DeletePV((pv)) 39 | So(err, ShouldBeNil) 40 | }) 41 | } 42 | 43 | //nolint:funlen 44 | func TestVG(t *testing.T) { 45 | Convey("testing lvm VGs", t, func() { 46 | sys := mockos.System("testdata/model_sys.json") 47 | lvm := mockos.LVM(sys) 48 | 49 | // Create a partition per disk and a PV 50 | disks, _ := sys.ScanAllDisks(nil) 51 | for _, d := range disks { 52 | name := d.Name + "1" 53 | err := sys.CreatePartition(d, disko.Partition{ 54 | Name: name, 55 | Number: 1, 56 | Start: 0, 57 | Last: d.Size, 58 | Type: partid.LinuxFS, 59 | }) 60 | 61 | So(err, ShouldBeNil) 62 | 63 | _, err = lvm.CreatePV(name) 64 | So(err, ShouldBeNil) 65 | } 66 | 67 | // Scan all SSDs 68 | ssds, err := sys.ScanAllDisks(func(d disko.Disk) bool { 69 | return d.Type == disko.SSD && d.Attachment == disko.RAID 70 | }) 71 | So(err, ShouldBeNil) 72 | So(ssds, ShouldNotBeEmpty) 73 | 74 | // Scan all PVs 75 | allPvs, err := lvm.ScanPVs(nil) 76 | So(err, ShouldBeNil) 77 | So(allPvs, ShouldNotBeEmpty) 78 | 79 | pvs, err := lvm.ScanPVs(func(p disko.PV) bool { 80 | name := strings.TrimSuffix(p.Name, "1") 81 | if _, ok := ssds[name]; ok { 82 | return true 83 | } 84 | return false 85 | }) 86 | So(err, ShouldBeNil) 87 | So(pvs, ShouldNotBeEmpty) 88 | 89 | pvlist := []disko.PV{} 90 | for _, pv := range pvs { 91 | pvlist = append(pvlist, pv) 92 | } 93 | 94 | // No vgs unless we create one 95 | vgs, err := lvm.ScanVGs(nil) 96 | So(err, ShouldBeNil) 97 | So(vgs, ShouldBeEmpty) 98 | 99 | // Should be able to create a new vg 100 | vg, err := lvm.CreateVG("ssd0", pvlist...) 101 | So(err, ShouldBeNil) 102 | So(vg.Name, ShouldEqual, "ssd0") 103 | So(lvm.HasVG("ssd0"), ShouldBeTrue) 104 | vgs, err = lvm.ScanVGs(func(v disko.VG) bool { return vg.Name == "ssd0" }) 105 | So(err, ShouldBeNil) 106 | So(len(vgs), ShouldEqual, 1) 107 | 108 | // Deleting PV should fail 109 | for _, pv := range pvlist { 110 | So(lvm.DeletePV(pv), ShouldBeError) 111 | } 112 | 113 | // Cannot create an existing vg 114 | _, err = lvm.CreateVG("ssd0", pvlist...) 115 | So(err, ShouldBeError) 116 | 117 | // Cannot create an existing vg with same pv 118 | _, err = lvm.CreateVG("ssd1", pvlist...) 119 | So(err, ShouldBeError) 120 | 121 | // Cannot extend a no existing vg 122 | err = lvm.ExtendVG("ssdaaa", pvlist...) 123 | So(err, ShouldBeError) 124 | 125 | // Cannot extend a vg with pvs already in use 126 | err = lvm.ExtendVG("ssd0", pvlist...) 127 | So(err, ShouldBeError) 128 | 129 | // Extend using new set of PVs 130 | sdaPv := allPvs["sda1"] 131 | So(sdaPv.Name, ShouldEqual, "sda1") 132 | err = lvm.ExtendVG("ssd0", sdaPv) 133 | So(err, ShouldBeNil) 134 | 135 | // Cannot remove an non existent vg 136 | err = lvm.RemoveVG("ssdx") 137 | So(err, ShouldBeError) 138 | 139 | // Remove the vg 140 | err = lvm.RemoveVG("ssd0") 141 | So(err, ShouldBeNil) 142 | So(lvm.HasVG("ssd0"), ShouldBeFalse) 143 | }) 144 | } 145 | 146 | //nolint:funlen 147 | func TestLV(t *testing.T) { 148 | Convey("test lvm lvs", t, func() { 149 | sys := mockos.System("testdata/model_sys.json") 150 | lvm := mockos.LVM(sys) 151 | So(sys, ShouldNotBeNil) 152 | So(lvm, ShouldNotBeNil) 153 | 154 | sdaPV, err := lvm.CreatePV("sda") 155 | So(err, ShouldBeNil) 156 | So(sdaPV.Name, ShouldEqual, "sda") 157 | sdbPV, err := lvm.CreatePV("sdb") 158 | So(err, ShouldBeNil) 159 | So(sdbPV.Name, ShouldEqual, "sdb") 160 | 161 | ssdVG, err := lvm.CreateVG("ssd0", sdbPV) 162 | So(err, ShouldBeNil) 163 | So(ssdVG.Name, ShouldEqual, "ssd0") 164 | 165 | // Cannot create lv on a unknown vg 166 | _, err = lvm.CreateLV("junk", "janardan", 0, disko.THICK) 167 | So(err, ShouldBeError) 168 | 169 | // Cannot create a really large LV 170 | _, err = lvm.CreateLV("ssd0", "lv1", uint64(0xFFFFFFFFFFFFFFFF), disko.THICK) 171 | So(err, ShouldBeError) 172 | 173 | // Create an LV 174 | size := ssdVG.Size / 2 175 | lv, err := lvm.CreateLV("ssd0", "lv1", size, disko.THICK) 176 | So(err, ShouldBeNil) 177 | So(lv.Name, ShouldEqual, "lv1") 178 | So(ssdVG.Volumes, ShouldNotBeEmpty) 179 | 180 | // Cannot create the same lv again 181 | _, err = lvm.CreateLV("ssd0", "lv1", size, disko.THICK) 182 | So(err, ShouldBeError) 183 | 184 | // Create another LV with remaining space 185 | lv, err = lvm.CreateLV("ssd0", "lv2", size, disko.THICK) 186 | So(err, ShouldBeNil) 187 | So(lv.Name, ShouldEqual, "lv2") 188 | So(ssdVG.Volumes, ShouldNotBeEmpty) 189 | So(lvm.HasLV("ssd0", "lv1"), ShouldBeTrue) 190 | 191 | // Cannot create any more lvs as there is no free space 192 | _, err = lvm.CreateLV("ssd0", "lv3", 1024, disko.THICK) 193 | So(err, ShouldBeError) 194 | 195 | // Cannot remove an non-existent lv 196 | So(lvm.RemoveLV("ssd0", "moon"), ShouldBeError) 197 | So(lvm.RemoveLV("sun", "moon"), ShouldBeError) 198 | 199 | // Rename the second LV to lvRenamed 200 | So(lvm.RenameLV("ssd0", "lv2", "lvRenamed"), ShouldBeNil) 201 | 202 | // Remove the second LV 203 | So(lvm.RemoveLV("ssd0", "lvRenamed"), ShouldBeNil) 204 | 205 | // Cannot extend LV that doesnt exist 206 | So(lvm.ExtendLV("sun", "moon", 1024), ShouldBeError) 207 | 208 | // Cannot extend LV to smaller size 209 | So(lvm.ExtendLV("ssd0", "lv1", 1024), ShouldBeError) 210 | 211 | // Extend LV to full size 212 | So(lvm.ExtendLV("ssd0", "lv1", ssdVG.Size), ShouldBeNil) 213 | 214 | // Cannot extend any more 215 | So(lvm.ExtendLV("ssd0", "lv1", ssdVG.Size+1024), ShouldBeError) 216 | 217 | ssdVGf := func() disko.VG { 218 | vgs, _ := lvm.ScanVGs(func(v disko.VG) bool { return v.Name == "ssd0" }) 219 | return vgs["ssd0"] 220 | } 221 | So(ssdVGf().Name, ShouldEqual, "ssd0") 222 | 223 | // Cannot encrypt non-existent LV 224 | So(lvm.CryptFormat("sun", "moon", "seemysecret"), ShouldBeError) 225 | 226 | // Cannot crypt open an unencrypted lv 227 | So(lvm.CryptOpen("ssd0", "lv1", "lv1_enc", "suttisecret"), ShouldBeError) 228 | So(lvm.CryptOpen("sun", "moon", "jupiter", "crapsecret"), ShouldBeError) 229 | So(lvm.CryptClose("ssd0", "lv1", "lv1_enc"), ShouldBeError) 230 | 231 | // crypt format an lv 232 | So(lvm.CryptFormat("ssd0", "lv1", "abcdedfgh"), ShouldBeNil) 233 | So(ssdVGf().Volumes["lv1"].Encrypted, ShouldBeTrue) 234 | 235 | // Crypt open the lv 236 | So(lvm.CryptOpen("ssd0", "lv1", "lv1_enc", "abcdedfgh"), ShouldBeNil) 237 | So(ssdVGf().Volumes["lv1"].DecryptedLVName, ShouldEqual, "lv1_enc") 238 | So(ssdVGf().Volumes["lv1"].DecryptedLVPath, ShouldEqual, "/dev/mapper/lv1_enc") 239 | 240 | // Crypt close the lv 241 | So(lvm.CryptClose("ssd0", "lv1", "lv1_enc"), ShouldBeNil) 242 | So(ssdVGf().Volumes["lv1"].DecryptedLVName, ShouldEqual, "") 243 | So(ssdVGf().Volumes["lv1"].DecryptedLVPath, ShouldEqual, "") 244 | 245 | // Cannot close an unopened lv 246 | So(lvm.CryptClose("blah", "blee", "blee_enc"), ShouldBeError) 247 | So(lvm.CryptClose("ssd0", "lv1", "lv1_enc"), ShouldBeError) 248 | }) 249 | } 250 | -------------------------------------------------------------------------------- /linux/system.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path" 8 | "regexp" 9 | "strings" 10 | "syscall" 11 | 12 | "golang.org/x/sys/unix" 13 | "machinerun.io/disko" 14 | "machinerun.io/disko/megaraid" 15 | "machinerun.io/disko/mpi3mr" 16 | "machinerun.io/disko/smartpqi" 17 | ) 18 | 19 | type linuxSystem struct { 20 | raidctrls []RAIDController 21 | } 22 | 23 | // System returns an linux specific implementation of disko.System interface. 24 | func System() disko.System { 25 | return &linuxSystem{ 26 | raidctrls: []RAIDController{ 27 | megaraid.CachingStorCli(), 28 | smartpqi.ArcConf(), 29 | mpi3mr.StorCli2(), 30 | }, 31 | } 32 | } 33 | 34 | // example below, of an azure vmbus disk that is ephemeral. 35 | // matching intent of /lib/udev/rules.d/66-azure-ephemeral.rules 36 | // /devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/VMBUS:01/00000000-0001-8899-0000-000000000000/ 37 | // 38 | // host1/target1:0:1/1:0:1:0/block/sdb 39 | var vmbusSyspathEphemeral = regexp.MustCompile(`.*/VMBUS:\d\d/00000000-0001-\d{4}-\d{4}-\d{12}/host.*`) 40 | 41 | func (ls *linuxSystem) ScanAllDisks(filter disko.DiskFilter) (disko.DiskSet, error) { 42 | var err error 43 | var dpaths = []string{} 44 | 45 | names, err := getDiskNames() 46 | if err != nil { 47 | return disko.DiskSet{}, err 48 | } 49 | 50 | for _, name := range names { 51 | dpath := path.Join("/dev", name) 52 | 53 | f, err := os.Open(dpath) 54 | if err != nil { 55 | // ENOMEDIUM will occur on a empty sd reader. 56 | if e, ok := err.(*os.PathError); ok { 57 | if e.Err == syscall.ENOMEDIUM { 58 | continue 59 | } 60 | } 61 | 62 | log.Printf("Skipping device %s: %v", name, err) 63 | 64 | continue 65 | } 66 | 67 | f.Close() 68 | 69 | dpaths = append(dpaths, dpath) 70 | } 71 | 72 | return ls.ScanDisks(filter, dpaths...) 73 | } 74 | 75 | func (ls *linuxSystem) ScanDisks(filter disko.DiskFilter, 76 | dpaths ...string) (disko.DiskSet, error) { 77 | disks := disko.DiskSet{} 78 | 79 | for _, dpath := range dpaths { 80 | disk, err := ls.ScanDisk(dpath) 81 | if err != nil { 82 | return disks, err 83 | } 84 | 85 | if filter(disk) { 86 | // Accepted so add to the set 87 | disks[disk.Name] = disk 88 | } 89 | } 90 | 91 | return disks, nil 92 | } 93 | 94 | func getDiskReadOnly(kname string) (bool, error) { 95 | syspath, err := getSysPathForBlockDevicePath(kname) 96 | if err != nil { 97 | return false, err 98 | } 99 | 100 | syspathReadOnly := syspath + "/ro" 101 | content, err := os.ReadFile(syspathReadOnly) 102 | 103 | if err != nil { 104 | return false, err 105 | } 106 | 107 | val := strings.TrimRight(string(content), "\n") 108 | 109 | if val == "1" { 110 | return true, nil 111 | } else if val == "0" { 112 | return false, nil 113 | } 114 | 115 | return false, fmt.Errorf("unexpected value '%s' found in %s", syspathReadOnly, val) 116 | } 117 | 118 | func getDiskProperties(d disko.UdevInfo) disko.PropertySet { 119 | props := disko.PropertySet{} 120 | 121 | if vmbusSyspathEphemeral.MatchString(d.SysPath) { 122 | props[disko.Ephemeral] = true 123 | } 124 | 125 | if d.Properties["ID_MODEL"] == "Amazon EC2 NVMe Instance Storage" { 126 | props[disko.Ephemeral] = true 127 | } 128 | 129 | return props 130 | } 131 | 132 | //nolint:funlen 133 | func (ls *linuxSystem) ScanDisk(devicePath string) (disko.Disk, error) { 134 | var err error 135 | var blockdev = true 136 | var ssize uint = sectorSize512 137 | var diskType disko.DiskType 138 | var attachType disko.AttachmentType 139 | var ro bool 140 | 141 | name, err := getKnameForBlockDevicePath(devicePath) 142 | 143 | if err != nil { 144 | name = path.Base(devicePath) 145 | blockdev = false 146 | } else { 147 | bss, err := getBlockSize(name) 148 | if err != nil { 149 | return disko.Disk{}, err 150 | } 151 | ssize = uint(bss) 152 | } 153 | 154 | udInfo := disko.UdevInfo{} 155 | 156 | if blockdev { 157 | udInfo, err = GetUdevInfo(name) 158 | if err != nil { 159 | return disko.Disk{}, err 160 | } 161 | 162 | attachType = getAttachType(udInfo) 163 | 164 | for _, ctrl := range ls.raidctrls { 165 | if IsSysPathRAID(udInfo.Properties["DEVPATH"], ctrl.DriverSysfsPath()) { 166 | // we know this is device is part of a raid, so if we cannot get 167 | // disk type we must return an error 168 | dType, err := ctrl.GetDiskType(devicePath) 169 | if err != nil { 170 | return disko.Disk{}, fmt.Errorf("failed to get diskType of %q from RAID controller: %s", devicePath, err) 171 | } 172 | 173 | attachType = disko.RAID 174 | diskType = dType 175 | 176 | break 177 | } 178 | } 179 | 180 | // check disk type if it wasn't on raid 181 | if attachType != disko.RAID { 182 | diskType, err = getDiskType(udInfo) 183 | if err != nil { 184 | return disko.Disk{}, fmt.Errorf("error while getting disk type: %s", err) 185 | } 186 | } 187 | 188 | ro, err = getDiskReadOnly(name) 189 | if err != nil { 190 | return disko.Disk{}, err 191 | } 192 | } else { 193 | diskType = disko.TYPEFILE 194 | attachType = disko.FILESYSTEM 195 | 196 | ro = false 197 | if err := unix.Access(devicePath, unix.W_OK); err == unix.EACCES { 198 | ro = true 199 | } else if err != nil { 200 | return disko.Disk{}, err 201 | } 202 | } 203 | 204 | properties := getDiskProperties(udInfo) 205 | 206 | disk := disko.Disk{ 207 | Name: name, 208 | Path: devicePath, 209 | SectorSize: ssize, 210 | ReadOnly: ro, 211 | UdevInfo: udInfo, 212 | Type: diskType, 213 | Attachment: attachType, 214 | Properties: properties, 215 | } 216 | 217 | fh, err := os.Open(devicePath) 218 | if err != nil { 219 | return disk, err 220 | } 221 | defer fh.Close() 222 | 223 | size, err := getFileSize(fh) 224 | if err != nil { 225 | return disk, err 226 | } 227 | 228 | disk.Size = size 229 | parts, tType, ssize, err := findPartitions(fh) 230 | 231 | if err != nil { 232 | return disk, err 233 | } 234 | 235 | disk.Table = tType 236 | 237 | if tType == disko.GPT && ssize != disk.SectorSize { 238 | if blockdev { 239 | return disk, fmt.Errorf( 240 | "disk %s has sector size %d and partition table sector size %d", 241 | disk.Path, disk.SectorSize, ssize) 242 | } 243 | 244 | disk.SectorSize = ssize 245 | } 246 | 247 | disk.Partitions = parts 248 | 249 | return disk, nil 250 | } 251 | 252 | func (ls *linuxSystem) CreatePartition(d disko.Disk, p disko.Partition) error { 253 | if err := addPartitionSet(d, disko.PartitionSet{p.Number: p}); err != nil { 254 | return err 255 | } 256 | 257 | return udevSettle() 258 | } 259 | 260 | func (ls *linuxSystem) CreatePartitions(d disko.Disk, pSet disko.PartitionSet) error { 261 | if err := addPartitionSet(d, pSet); err != nil { 262 | return err 263 | } 264 | 265 | return udevSettle() 266 | } 267 | 268 | func (ls *linuxSystem) DeletePartition(d disko.Disk, number uint) error { 269 | if err := deletePartitions(d, []uint{number}); err != nil { 270 | return err 271 | } 272 | 273 | return udevSettle() 274 | } 275 | 276 | func (ls *linuxSystem) UpdatePartition(d disko.Disk, p disko.Partition) error { 277 | if err := updatePartitions(d, disko.PartitionSet{p.Number: p}); err != nil { 278 | return err 279 | } 280 | 281 | return udevSettle() 282 | } 283 | 284 | func (ls *linuxSystem) UpdatePartitions(d disko.Disk, pSet disko.PartitionSet) error { 285 | if err := updatePartitions(d, pSet); err != nil { 286 | return err 287 | } 288 | 289 | return udevSettle() 290 | } 291 | 292 | func (ls *linuxSystem) Wipe(d disko.Disk) error { 293 | if err := wipeDisk(d); err != nil { 294 | return err 295 | } 296 | 297 | return udevSettle() 298 | } 299 | 300 | func (ls *linuxSystem) GetDiskType(path string, udInfo disko.UdevInfo) (disko.DiskType, error) { 301 | for _, ctrl := range ls.raidctrls { 302 | if IsSysPathRAID(udInfo.Properties["DEVPATH"], ctrl.DriverSysfsPath()) { 303 | dType, err := ctrl.GetDiskType(path) 304 | if err != nil { 305 | return disko.HDD, fmt.Errorf("failed to get diskType of %q from RAID controller: %s", path, err) 306 | } 307 | 308 | return dType, nil 309 | } 310 | } 311 | return getDiskType(udInfo) 312 | } 313 | -------------------------------------------------------------------------------- /mockos/lvm.go: -------------------------------------------------------------------------------- 1 | package mockos 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | 7 | "machinerun.io/disko" 8 | ) 9 | 10 | type mockLVM struct { 11 | VGs disko.VGSet `json:"vgs"` 12 | PVs disko.PVSet `json:"pvs"` 13 | sys disko.System 14 | freePVs disko.PVSet 15 | } 16 | 17 | // LVM return mock lvm implementation. 18 | func LVM(sys disko.System) disko.VolumeManager { 19 | return &mockLVM{ 20 | VGs: disko.VGSet{}, 21 | PVs: disko.PVSet{}, 22 | sys: sys, 23 | freePVs: disko.PVSet{}, 24 | } 25 | } 26 | 27 | func (lvm *mockLVM) ScanPVs(filter disko.PVFilter) (disko.PVSet, error) { 28 | pvs := disko.PVSet{} 29 | 30 | for n, pv := range lvm.PVs { 31 | if filter == nil || filter(pv) { 32 | pvs[n] = pv 33 | } 34 | } 35 | 36 | return pvs, nil 37 | } 38 | 39 | func (lvm *mockLVM) ScanVGs(filter disko.VGFilter) (disko.VGSet, error) { 40 | vgs := disko.VGSet{} 41 | 42 | for n, vg := range lvm.VGs { 43 | if filter == nil || filter(vg) { 44 | vgs[n] = vg 45 | } 46 | } 47 | 48 | return vgs, nil 49 | } 50 | 51 | func hasPartition(disks disko.DiskSet, name string) bool { 52 | for _, d := range disks { 53 | for _, p := range d.Partitions { 54 | if p.Name == name { 55 | return true 56 | } 57 | } 58 | } 59 | 60 | return false 61 | } 62 | 63 | func (lvm *mockLVM) CreatePV(deviceName string) (disko.PV, error) { 64 | disks, _ := lvm.sys.ScanAllDisks(func(d disko.Disk) bool { return true }) 65 | d, ok := disks[deviceName] 66 | 67 | if !ok { 68 | // The device is not a disk, lets check if it is a partition. 69 | if !hasPartition(disks, deviceName) { 70 | return disko.PV{}, fmt.Errorf("disk %s does not exist", deviceName) 71 | } 72 | } 73 | 74 | if _, ok := lvm.PVs[deviceName]; ok { 75 | return disko.PV{}, fmt.Errorf("pv %s already exists", deviceName) 76 | } 77 | 78 | pv := disko.PV{ 79 | Name: deviceName, 80 | Path: path.Join("dev", deviceName), 81 | Size: d.Size, 82 | FreeSize: d.Size, 83 | } 84 | 85 | lvm.freePVs[pv.Name] = pv 86 | lvm.PVs[pv.Name] = pv 87 | 88 | return pv, nil 89 | } 90 | 91 | func (lvm *mockLVM) DeletePV(pv disko.PV) error { 92 | if _, ok := lvm.PVs[pv.Name]; !ok { 93 | return fmt.Errorf("pv %s does not exist", pv.Name) 94 | } 95 | 96 | // PV must not be used by any vg to delete 97 | if _, ok := lvm.freePVs[pv.Name]; !ok { 98 | return fmt.Errorf("pv %s is in use", pv.Name) 99 | } 100 | 101 | delete(lvm.PVs, pv.Name) 102 | delete(lvm.freePVs, pv.Name) 103 | 104 | return nil 105 | } 106 | 107 | func (lvm *mockLVM) HasPV(name string) bool { 108 | _, ok := lvm.PVs[name] 109 | return ok 110 | } 111 | 112 | func (lvm *mockLVM) CreateVG(name string, pvs ...disko.PV) (disko.VG, error) { 113 | if _, ok := lvm.VGs[name]; ok { 114 | return disko.VG{}, fmt.Errorf("vg %s already exists", name) 115 | } 116 | 117 | pvSet := disko.PVSet{} 118 | size := uint64(0) 119 | 120 | for _, pv := range pvs { 121 | if _, ok := lvm.freePVs[pv.Name]; !ok { 122 | // pv already used by some other vg 123 | return disko.VG{}, fmt.Errorf("pv %s already in use", pv.Name) 124 | } 125 | 126 | // delete the PV from list and add it to this vg list 127 | delete(lvm.freePVs, pv.Name) 128 | pvSet[pv.Name] = pv 129 | 130 | size += pv.Size 131 | } 132 | 133 | vg := disko.VG{ 134 | Name: name, 135 | Size: size, 136 | Volumes: disko.LVSet{}, 137 | FreeSpace: size, 138 | PVs: pvSet, 139 | } 140 | 141 | lvm.VGs[name] = vg 142 | 143 | return vg, nil 144 | } 145 | 146 | func (lvm *mockLVM) ExtendVG(vgName string, pvs ...disko.PV) error { 147 | vg, ok := lvm.VGs[vgName] 148 | if !ok { 149 | return fmt.Errorf("vg %s does not exist", vgName) 150 | } 151 | 152 | for _, pv := range pvs { 153 | if _, ok := lvm.freePVs[pv.Name]; !ok { 154 | // pv already used by some other vg 155 | return fmt.Errorf("pv %s already in use", pv.Name) 156 | } 157 | } 158 | 159 | // Delete all the added pvs from the free list 160 | for _, pv := range pvs { 161 | delete(lvm.freePVs, pv.Name) 162 | vg.PVs[pv.Name] = pv 163 | vg.Size += pv.Size 164 | vg.FreeSpace += pv.FreeSize 165 | } 166 | 167 | lvm.VGs[vg.Name] = vg 168 | 169 | return nil 170 | } 171 | 172 | func (lvm *mockLVM) RemoveVG(vgName string) error { 173 | vg, ok := lvm.VGs[vgName] 174 | if !ok { 175 | return fmt.Errorf("vg %s does not exist", vgName) 176 | } 177 | 178 | for _, pv := range vg.PVs { 179 | // Add all the pvs from this vg into the free list 180 | lvm.freePVs[pv.Name] = pv 181 | } 182 | 183 | // Delete this VG from lvm 184 | delete(lvm.VGs, vgName) 185 | 186 | return nil 187 | } 188 | 189 | func (lvm *mockLVM) HasVG(vgName string) bool { 190 | _, ok := lvm.VGs[vgName] 191 | return ok 192 | } 193 | 194 | func (lvm *mockLVM) CryptFormat(vgName string, lvName string, 195 | key string) error { 196 | vg, lv, err := lvm.findLV(vgName, lvName) 197 | if err != nil { 198 | return fmt.Errorf("lv %s does not exist", lvName) 199 | } 200 | 201 | lv.Encrypted = true 202 | vg.Volumes[lvName] = lv 203 | 204 | return nil 205 | } 206 | 207 | func (lvm *mockLVM) CryptOpen(vgName string, lvName string, 208 | decryptedName string, key string) error { 209 | vg, lv, err := lvm.findLV(vgName, lvName) 210 | if err != nil { 211 | return fmt.Errorf("lv %s does not exist", lvName) 212 | } 213 | 214 | if !lv.Encrypted { 215 | return fmt.Errorf("lv %s is not encrypted", lvName) 216 | } 217 | 218 | lv.DecryptedLVName = decryptedName 219 | lv.DecryptedLVPath = path.Join("/dev/mapper", decryptedName) 220 | vg.Volumes[lvName] = lv 221 | 222 | return nil 223 | } 224 | 225 | func (lvm *mockLVM) CryptClose(vgName string, lvName string, 226 | decryptedName string) error { 227 | vg, lv, err := lvm.findLV(vgName, lvName) 228 | if err != nil { 229 | return fmt.Errorf("lv %s does not exist", lvName) 230 | } 231 | 232 | if !lv.Encrypted { 233 | return fmt.Errorf("lv %s is not encrypted", lvName) 234 | } 235 | 236 | if lv.DecryptedLVName == "" || lv.DecryptedLVPath == "" { 237 | return fmt.Errorf("lv %s is not opened", lvName) 238 | } 239 | 240 | lv.DecryptedLVName = "" 241 | lv.DecryptedLVPath = "" 242 | vg.Volumes[lvName] = lv 243 | 244 | return nil 245 | } 246 | 247 | func (lvm *mockLVM) CreateLV(vgName string, name string, size uint64, 248 | lvType disko.LVType) (disko.LV, error) { 249 | vg, _, err := lvm.findLV(vgName, name) 250 | if err == nil { 251 | return disko.LV{}, fmt.Errorf("lv %s already exists", name) 252 | } 253 | 254 | vg, ok := lvm.VGs[vgName] 255 | if !ok { 256 | return disko.LV{}, fmt.Errorf("vg %s does not exist", vgName) 257 | } 258 | 259 | if vg.FreeSpace < size { 260 | return disko.LV{}, fmt.Errorf("vg %s does not have enough space", vgName) 261 | } 262 | 263 | lv := disko.LV{ 264 | Name: name, 265 | Size: size, 266 | Type: lvType, 267 | VGName: vgName, 268 | Encrypted: false, 269 | } 270 | 271 | // Add the lv to vg and discount the freespace 272 | vg.Volumes[name] = lv 273 | vg.FreeSpace -= size 274 | 275 | lvm.VGs[vg.Name] = vg 276 | 277 | return lv, nil 278 | } 279 | 280 | func (lvm *mockLVM) RenameLV(vgName string, lvName string, newLvName string) error { 281 | vg, lv, err := lvm.findLV(vgName, lvName) 282 | if err != nil { 283 | return err 284 | } 285 | 286 | delete(vg.Volumes, lvName) 287 | 288 | vg.Volumes[newLvName] = lv 289 | lv.Name = newLvName 290 | 291 | return nil 292 | } 293 | 294 | func (lvm *mockLVM) RemoveLV(vgName string, lvName string) error { 295 | vg, lv, err := lvm.findLV(vgName, lvName) 296 | if err != nil { 297 | return err 298 | } 299 | 300 | // Delete the LV and reclaim the free space 301 | delete(vg.Volumes, lvName) 302 | vg.FreeSpace += lv.Size 303 | 304 | lvm.VGs[vg.Name] = vg 305 | 306 | return nil 307 | } 308 | 309 | func (lvm *mockLVM) ExtendLV(vgName string, lvName string, 310 | newSize uint64) error { 311 | vg, lv, err := lvm.findLV(vgName, lvName) 312 | if err != nil { 313 | return err 314 | } 315 | 316 | if newSize < lv.Size { 317 | return fmt.Errorf("lv size cannot be reduced") 318 | } 319 | 320 | deltaSize := newSize - lv.Size 321 | 322 | if vg.FreeSpace < deltaSize { 323 | return fmt.Errorf("vg %s does not have enough space", vg.Name) 324 | } 325 | 326 | // allocate the space from the vg to this lv 327 | vg.FreeSpace -= deltaSize 328 | lv.Size += deltaSize 329 | 330 | return nil 331 | } 332 | 333 | func (lvm *mockLVM) HasLV(vgName string, name string) bool { 334 | _, _, err := lvm.findLV(vgName, name) 335 | return err == nil 336 | } 337 | 338 | func (lvm *mockLVM) findLV(vgName string, lvName string) (disko.VG, disko.LV, error) { 339 | vg, ok := lvm.VGs[vgName] 340 | if !ok { 341 | return disko.VG{}, disko.LV{}, fmt.Errorf("vg %s not found", vgName) 342 | } 343 | 344 | lv, ok := vg.Volumes[lvName] 345 | if !ok { 346 | return disko.VG{}, disko.LV{}, fmt.Errorf("lv %s not found", lvName) 347 | } 348 | 349 | return vg, lv, nil 350 | } 351 | -------------------------------------------------------------------------------- /demo/misc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "regexp" 8 | 9 | "github.com/urfave/cli/v2" 10 | "machinerun.io/disko" 11 | "machinerun.io/disko/linux" 12 | "machinerun.io/disko/partid" 13 | ) 14 | 15 | const partitionName = "updown" 16 | const maxParts = 128 17 | const mySecret = "passw0rd" 18 | 19 | //nolint:gochecknoglobals 20 | var miscCommands = cli.Command{ 21 | Name: "misc", 22 | Usage: "miscellaneous test/debug", 23 | Subcommands: []*cli.Command{ 24 | { 25 | Name: "updown", 26 | Usage: "Create a partition table, vg, lv, take it all down", 27 | Action: miscUpDown, 28 | Flags: []cli.Flag{ 29 | &cli.BoolFlag{ 30 | Name: "skip-lvm", 31 | Value: false, 32 | Usage: "Do not do lvm operations", 33 | }, 34 | &cli.BoolFlag{ 35 | Name: "skip-pvcreate", 36 | Value: false, 37 | Usage: "Do not do pvcreate separately from vgcreate", 38 | }, 39 | &cli.BoolFlag{ 40 | Name: "skip-partition", 41 | Value: false, 42 | Usage: "Do not create and remove partition in the loop", 43 | }, 44 | &cli.BoolFlag{ 45 | Name: "skip-extend", 46 | Value: false, 47 | Usage: "Do not extend the volume", 48 | }, 49 | &cli.BoolFlag{ 50 | Name: "skip-luks", 51 | Value: false, 52 | Usage: fmt.Sprintf("Do not setup luks (password is '%s')", mySecret), 53 | }, 54 | &cli.BoolFlag{ 55 | Name: "skip-teardown", 56 | Value: false, 57 | Usage: "Do not tear down on final run - pv, vg, lv, luks will all be still up.", 58 | }, 59 | &cli.IntFlag{ 60 | Name: "loops", 61 | Value: 1, 62 | Usage: fmt.Sprintf("Do not create a partition - requires one named '%s'", partitionName), 63 | }, 64 | }, 65 | }, 66 | }, 67 | } 68 | 69 | func pathExists(fpath string) bool { 70 | _, err := os.Stat(fpath) 71 | if err != nil && os.IsNotExist(err) { 72 | return false 73 | } 74 | 75 | return true 76 | } 77 | 78 | func findPartInfo(diskPath string) (disko.Partition, error) { 79 | const mysize = 200 * disko.Mebibyte 80 | var err error 81 | 82 | mysys := linux.System() 83 | 84 | disk, err := mysys.ScanDisk(diskPath) 85 | if err != nil { 86 | return disko.Partition{}, fmt.Errorf("failed to scan %s: %s", diskPath, err) 87 | } 88 | 89 | // try to find by name (previous failed run) 90 | for i := uint(1); i < maxParts; i++ { 91 | if disk.Partitions[i].Name == partitionName { 92 | fmt.Printf("Using existing partition number %d with name %s.\n", i, partitionName) 93 | return disk.Partitions[i], nil 94 | } 95 | } 96 | 97 | // no --part-number given or --part-number=0 - find a number and space. 98 | partNum := uint(maxParts) 99 | 100 | for i := uint(1); i < maxParts; i++ { 101 | if _, ok := disk.Partitions[i]; !ok { 102 | partNum = i 103 | break 104 | } 105 | } 106 | 107 | if partNum == maxParts { 108 | return disko.Partition{}, fmt.Errorf("unable to find empty partition number on %s", disk.Path) 109 | } 110 | 111 | fss := disk.FreeSpacesWithMin(mysize) 112 | if len(fss) < 1 { 113 | return disko.Partition{}, fmt.Errorf("did not find usable freespace on %s", disk.Path) 114 | } 115 | 116 | part := disko.Partition{ 117 | Type: partid.LinuxLVM, 118 | Name: partitionName, 119 | ID: disko.GenGUID(), 120 | Number: partNum, 121 | Start: fss[0].Start, 122 | Last: fss[0].Start + mysize - 1, 123 | } 124 | 125 | return part, nil 126 | } 127 | 128 | //nolint:gocognit, gocyclo, funlen 129 | func miscUpDown(c *cli.Context) error { 130 | fname := c.Args().First() 131 | 132 | var err error 133 | var numRuns = c.Int("loops") 134 | var doPartition = !c.Bool("skip-partition") 135 | var doCreatePV = !c.Bool("skip-pvcreate") 136 | var doLvm = !c.Bool("skip-lvm") 137 | var doLuks = !c.Bool("skip-luks") 138 | var doExtend = !c.Bool("skip-extend") 139 | var skipTeardown = c.Bool("skip-teardown") 140 | var part disko.Partition 141 | var pv disko.PV 142 | var vg disko.VG 143 | var lv disko.LV 144 | 145 | var createMiB, extendMiB uint64 = 100, 48 146 | 147 | if fname == "" { 148 | return fmt.Errorf("must provide disk/file to partition") 149 | } 150 | 151 | part, err = findPartInfo(fname) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | mysys := linux.System() 157 | myvmgr := linux.VolumeManager() 158 | 159 | disk, err := mysys.ScanDisk(fname) 160 | 161 | if err != nil { 162 | return fmt.Errorf("failed to scan %s: %s", fname, err) 163 | } 164 | 165 | endsWithDigit := regexp.MustCompile("[0-9]$") 166 | partSep := "" 167 | 168 | if (disk.Attachment == disko.PCIE || 169 | disk.Attachment == disko.NBD || 170 | disk.Attachment == disko.LOOP) || endsWithDigit.MatchString(disk.Path) { 171 | partSep = "p" 172 | } 173 | 174 | partPath := fmt.Sprintf("%s%s%d", disk.Path, partSep, part.Number) 175 | partName := fmt.Sprintf("%s%s%d", path.Base(disk.Path), partSep, part.Number) 176 | 177 | if doPartition { 178 | if pathExists(partPath) { 179 | if err = mysys.DeletePartition(disk, part.Number); err != nil { 180 | return err 181 | } 182 | 183 | fmt.Printf("deleted existing partition %d on %s\n", part.Number, disk.Path) 184 | } 185 | } else if !pathExists(partPath) { 186 | err = mysys.CreatePartition(disk, part) 187 | if err != nil { 188 | return err 189 | } 190 | delPart := func() { 191 | if pathExists(partPath) { 192 | fmt.Printf("Deleting partition %s %d\n", disk.Path, part.Number) 193 | if err := mysys.DeletePartition(disk, part.Number); err != nil { 194 | fmt.Printf("that went bad: %s\n", err) 195 | } 196 | } 197 | } 198 | 199 | defer delPart() 200 | } 201 | 202 | if disk, err = mysys.ScanDisk(fname); err != nil { 203 | return err 204 | } 205 | 206 | fmt.Printf("numruns=%d partition=%t createpv=%t lvm=%t luks=%t extend=%t\n%s\n", 207 | numRuns, doPartition, doCreatePV, doLvm, doLuks, doExtend, disk.Details()) 208 | 209 | luksSuffix := "_crypt" 210 | 211 | for i := 0; i < numRuns; i++ { 212 | fmt.Printf("[%d] starting %s %d\n", i, disk.Path, part.Number) 213 | 214 | if doPartition { 215 | err = mysys.CreatePartition(disk, part) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | fmt.Printf("[%d] created partition %d\n", i, part.Number) 221 | } 222 | 223 | if !pathExists(partPath) { 224 | fmt.Printf("partition path %s did not exist.", partPath) 225 | 226 | return fmt.Errorf("should have existed") 227 | } 228 | 229 | if doLvm { 230 | if doCreatePV { 231 | pv, err = myvmgr.CreatePV(partPath) 232 | if err != nil { 233 | fmt.Printf("failed to createPV(%s): %s", partPath, err) 234 | return err 235 | } 236 | 237 | fmt.Printf("[%d] created PV %s: %v\n", i, partPath, pv.UUID) 238 | } else { 239 | pv = disko.PV{Name: partName, Path: partPath} 240 | } 241 | 242 | vg, err = myvmgr.CreateVG("myvg0", pv) 243 | if err != nil { 244 | fmt.Printf("Failed creating vg on %s\n", pv.Name) 245 | 246 | return err 247 | } 248 | 249 | fmt.Printf("[%d] created VG %s\n", i, "myvg0") 250 | 251 | lv, err = myvmgr.CreateLV(vg.Name, "mylv0", createMiB*disko.Mebibyte, disko.THICK) 252 | if err != nil { 253 | fmt.Printf("Failed creating lv %s on %s\n", "mylv0", "myvg0") 254 | 255 | return err 256 | } 257 | 258 | fmt.Printf("[%d] created LV %s/%s (%d) luks=%t\n", 259 | i, vg.Name, lv.Name, lv.Size/disko.Mebibyte, doLuks) 260 | 261 | luksName := vg.Name + "_" + lv.Name + luksSuffix 262 | 263 | if doLuks { 264 | if err = myvmgr.CryptFormat(vg.Name, lv.Name, mySecret); err != nil { 265 | fmt.Printf("Failed to CryptFormat %s/%s\n", vg.Name, lv.Name) 266 | return err 267 | } 268 | 269 | if err = myvmgr.CryptOpen(vg.Name, lv.Name, luksName, mySecret); err != nil { 270 | fmt.Printf("Failed to CryptOpen %s/%s %s\n", vg.Name, lv.Name, luksName) 271 | return err 272 | } 273 | 274 | fmt.Printf("[%d] created luks device %s\n", i, luksName) 275 | } 276 | 277 | if doExtend { 278 | if err := myvmgr.ExtendLV(vg.Name, lv.Name, (createMiB+extendMiB)*disko.Mebibyte); err != nil { 279 | fmt.Printf("Failed to Extend(%s, %s, %dMiB)\n", vg.Name, lv.Name, extendMiB) 280 | return err 281 | } 282 | 283 | fmt.Printf("[%d] extended %s/%s\n", i, vg.Name, lv.Name) 284 | } 285 | 286 | if skipTeardown && i+1 == numRuns { 287 | fmt.Printf("Leaving everything up on final run.\n") 288 | continue 289 | } 290 | 291 | if doLuks { 292 | if err = myvmgr.CryptClose(vg.Name, lv.Name, luksName); err != nil { 293 | return err 294 | } 295 | } 296 | 297 | err = myvmgr.RemoveVG(vg.Name) 298 | if err != nil { 299 | fmt.Printf("Failed removing vg %s: %s\n", "mylv0", err) 300 | return err 301 | } 302 | 303 | err = myvmgr.DeletePV(pv) 304 | if err != nil { 305 | fmt.Printf("failed to DeletePV(%s): %s\n", partPath, err) 306 | return err 307 | } 308 | 309 | fmt.Printf("[%d] deleted PV %s\n", i, partPath) 310 | } 311 | 312 | if doPartition { 313 | err = mysys.DeletePartition(disk, part.Number) 314 | if err != nil { 315 | return err 316 | } 317 | 318 | if pathExists(partPath) { 319 | fmt.Printf("After DeletePartition %d, %s did exist.", part.Number, partPath) 320 | return fmt.Errorf("should not have existed") 321 | } 322 | 323 | fmt.Printf("[%d] deleted partition %s\n", i, partPath) 324 | } 325 | } 326 | 327 | return nil 328 | } 329 | -------------------------------------------------------------------------------- /linux/util.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "path" 11 | "path/filepath" 12 | "strconv" 13 | "strings" 14 | "syscall" 15 | 16 | "github.com/pkg/errors" 17 | "machinerun.io/disko" 18 | ) 19 | 20 | // GetUdevInfo return a UdevInfo for the device with kernel name kname. 21 | func GetUdevInfo(kname string) (disko.UdevInfo, error) { 22 | out, stderr, rc := runCommandWithOutputErrorRc( 23 | "udevadm", "info", "--query=all", "--export", "--name="+kname) 24 | 25 | info := disko.UdevInfo{Name: kname} 26 | 27 | if rc != 0 { 28 | return info, 29 | fmt.Errorf("error querying kname '%s' [%d]: %s", kname, rc, stderr) 30 | } 31 | 32 | return info, parseUdevInfo(out, &info) 33 | } 34 | 35 | func parseUdevInfo(out []byte, info *disko.UdevInfo) error { 36 | var toks [][]byte 37 | var payload, s string 38 | var err error 39 | 40 | if info.Properties == nil { 41 | info.Properties = map[string]string{} 42 | } 43 | 44 | for _, line := range bytes.Split(out, []byte("\n")) { 45 | if len(line) == 0 { 46 | continue 47 | } 48 | 49 | toks = bytes.SplitN(line, []byte(": "), 2) 50 | 51 | if len(toks) != 2 { 52 | log.Printf("parseUdevInfo: ignoring unparsable line %q\n", line) 53 | continue 54 | } 55 | 56 | payload = string(toks[1]) 57 | switch toks[0][0] { 58 | case 'P': 59 | // Device path in /sys/ 60 | info.SysPath = payload 61 | case 'M': 62 | // Device name in /sys/ (i.e. the last component of "P:") 63 | continue 64 | case 'R': 65 | // Device number in /sys/ (i.e. the numeric suffix of the last component of "P:") 66 | continue 67 | case 'U': 68 | // Kernel subsystem 69 | continue 70 | case 'T': 71 | // Kernel device type with subsystem 72 | continue 73 | case 'D': 74 | // Kernel device node major/minor 75 | continue 76 | case 'I': 77 | // Network interface index 78 | continue 79 | case 'N': 80 | // Kernel device node name 81 | info.Name = payload 82 | case 'L': 83 | // Device node symlink priority 84 | continue 85 | case 'S': 86 | // Device node symlink 87 | info.Symlinks = append(info.Symlinks, strings.Split(payload, " ")...) 88 | case 'Q': 89 | // Block device sequence number (DISKSEQ) 90 | continue 91 | case 'V': 92 | // Attached driver 93 | continue 94 | case 'J': 95 | // Device ID 96 | continue 97 | case 'E': 98 | kv := strings.SplitN(payload, "=", 2) 99 | // use of Unquote is to decode \x20, \x2f and friends. 100 | // example: ID_MODEL_ENC=Integrated\x20Camera 101 | // and values often have trailing whitespace. 102 | s, err = strconv.Unquote("\"" + kv[1] + "\"") 103 | if err != nil { 104 | return fmt.Errorf("failed to unquote %#v: %s", kv[1], err) 105 | } 106 | 107 | info.Properties[kv[0]] = strings.TrimSpace(s) 108 | default: 109 | log.Printf("parseUdevInfo: ignoring unknown udevadm info prefix %q in %q\n", toks[0][0], line) 110 | } 111 | } 112 | 113 | return nil 114 | } 115 | 116 | func getCommandErrorRCDefault(err error, rcError int) int { 117 | if err == nil { 118 | return 0 119 | } 120 | 121 | exitError, ok := err.(*exec.ExitError) 122 | if ok { 123 | if status, ok := exitError.Sys().(syscall.WaitStatus); ok { 124 | return status.ExitStatus() 125 | } 126 | } 127 | 128 | return rcError 129 | } 130 | 131 | func getCommandErrorRC(err error) int { 132 | return getCommandErrorRCDefault(err, 127) 133 | } 134 | 135 | func cmdError(args []string, out []byte, err []byte, rc int) error { 136 | if rc == 0 { 137 | return nil 138 | } 139 | 140 | return errors.New(cmdString(args, out, err, rc)) 141 | } 142 | 143 | func cmdString(args []string, out []byte, err []byte, rc int) string { 144 | tlen := len(err) 145 | if tlen == 0 || err[tlen-1] != '\n' { 146 | err = append(err, '\n') 147 | } 148 | 149 | tlen = len(out) 150 | if tlen == 0 || out[tlen-1] != '\n' { 151 | out = append(out, '\n') 152 | } 153 | 154 | return fmt.Sprintf( 155 | "command returned %d:\n cmd: %v\n out: %s err: %s", 156 | rc, args, out, err) 157 | } 158 | 159 | func runCommandWithOutputErrorRc(args ...string) ([]byte, []byte, int) { 160 | cmd := exec.Command(args[0], args[1:]...) //nolint:gosec 161 | var stdout, stderr bytes.Buffer 162 | cmd.Stdout = &stdout 163 | cmd.Stderr = &stderr 164 | err := cmd.Run() 165 | 166 | return stdout.Bytes(), stderr.Bytes(), getCommandErrorRC(err) 167 | } 168 | 169 | func runCommand(args ...string) error { 170 | out, err, rc := runCommandWithOutputErrorRc(args...) 171 | return cmdError(args, out, err, rc) 172 | } 173 | 174 | func runCommandWithOutputErrorRcStdin(input string, args ...string) ([]byte, []byte, int) { 175 | cmd := exec.Command(args[0], args[1:]...) //nolint:gosec 176 | 177 | stdin, err := cmd.StdinPipe() 178 | if err != nil { 179 | log.Fatal(err) 180 | } 181 | 182 | go func() { 183 | defer stdin.Close() 184 | io.WriteString(stdin, input) //nolint:errcheck 185 | }() 186 | 187 | var stdout, stderr bytes.Buffer 188 | cmd.Stdout = &stdout 189 | cmd.Stderr = &stderr 190 | err = cmd.Run() 191 | 192 | return stdout.Bytes(), stderr.Bytes(), getCommandErrorRC(err) 193 | } 194 | 195 | func runCommandStdin(input string, args ...string) error { 196 | out, err, rc := runCommandWithOutputErrorRcStdin(input, args...) 197 | return cmdError(args, out, err, rc) 198 | } 199 | 200 | func udevSettle() error { 201 | return runCommand("udevadm", "settle") 202 | } 203 | 204 | func runCommandSettled(args ...string) error { 205 | err := runCommand(args...) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | return udevSettle() 211 | } 212 | 213 | func pathExists(d string) bool { 214 | _, err := os.Stat(d) 215 | if err != nil && os.IsNotExist(err) { 216 | return false 217 | } 218 | 219 | return true 220 | } 221 | 222 | func getBlockSize(dev string) (uint64, error) { 223 | path := path.Join("/sys/block", path.Base(dev), "queue/logical_block_size") 224 | 225 | content, err := os.ReadFile(path) 226 | if os.IsNotExist(err) { 227 | return uint64(0), errors.Wrapf(err, "%s did not exist: is %s a disk?", path, dev) 228 | } 229 | 230 | if err != nil { 231 | return uint64(0), errors.Wrapf(err, "Failed to read size for '%s'", dev) 232 | } 233 | 234 | d := strings.TrimSpace(string(content)) 235 | 236 | v, err := strconv.Atoi(d) 237 | if err != nil { 238 | return uint64(0), 239 | errors.Wrapf(err, 240 | "getBlockSize(%s): failed to convert '%s' to int", dev, d) 241 | } 242 | 243 | return uint64(v), nil 244 | } 245 | 246 | func getFileSize(file *os.File) (uint64, error) { 247 | var err error 248 | var cur, pos int64 249 | 250 | // read the current position so we can set it back before return 251 | if cur, err = file.Seek(0, io.SeekCurrent); err != nil { 252 | return 0, err 253 | } 254 | 255 | if pos, err = file.Seek(0, io.SeekEnd); err != nil { 256 | return 0, err 257 | } 258 | 259 | if _, err = file.Seek(cur, io.SeekStart); err != nil { 260 | return 0, err 261 | } 262 | 263 | return uint64(pos), nil 264 | } 265 | 266 | func lvPath(vgName, lvName string) string { 267 | return path.Join("/dev", vgName, lvName) 268 | } 269 | 270 | func vgLv(vgName, lvName string) string { 271 | return path.Join(vgName, lvName) 272 | } 273 | 274 | // Ceiling returns the smallest integer equal to or larger than val that is evenly 275 | // divisible by unit. 276 | func Ceiling(val, unit uint64) uint64 { 277 | if val%unit == 0 { 278 | return val 279 | } 280 | 281 | return ((val + unit) / unit) * unit 282 | } 283 | 284 | // Floor returns the largest integer equal to or less than val that is evenly 285 | // divisible by unit. 286 | func Floor(val, unit uint64) uint64 { 287 | if val%unit == 0 { 288 | return val 289 | } 290 | 291 | return (val / unit) * unit 292 | } 293 | 294 | // IsSysPathRAID - is this sys path (udevadm info's DEVPATH) on a scsi controller. 295 | // 296 | // syspath will look something like 297 | // /devices/pci0000:3a/0000:3a:02.0/0000:3c:00.0/host0/target0:2:2/0:2:2:0/block/sdc 298 | func IsSysPathRAID(syspath string, driverSysPath string) bool { 299 | if !strings.HasPrefix(syspath, "/sys") { 300 | syspath = "/sys" + syspath 301 | } 302 | 303 | if !strings.Contains(syspath, "/host") { 304 | return false 305 | } 306 | 307 | fp, err := filepath.EvalSymlinks(syspath) 308 | if err != nil { 309 | fmt.Printf("seriously? %s\n", err) 310 | return false 311 | } 312 | 313 | for _, path := range GetSysPaths(driverSysPath) { 314 | if strings.HasPrefix(fp, path) { 315 | return true 316 | } 317 | } 318 | 319 | return false 320 | } 321 | 322 | // NameByDiskID - return the linux name (sda) for the disk with given DiskID 323 | func NameByDiskID(driverSysPath string, id int) (string, error) { 324 | // given ID, we expect a single file in: 325 | // /0000:05:00.0/host0/target0:0:/0:0::0/block/ 326 | // Note: This does not work for some controllers such as a MegaRAID SAS3508 327 | // See https://github.com/project-machine/disko/issues/101 328 | idStr := fmt.Sprintf("%d", id) 329 | blkDir := driverSysPath + "/*/host*/target0:0:" + idStr + "/0:0:" + idStr + ":0/block/*" 330 | matches, err := filepath.Glob(blkDir) 331 | 332 | if err != nil { 333 | return "", err 334 | } 335 | 336 | if len(matches) != 1 { 337 | return "", fmt.Errorf("found %d matches to %s", len(matches), blkDir) 338 | } 339 | 340 | return path.Base(matches[0]), nil 341 | } 342 | 343 | func GetSysPaths(driverSysPath string) []string { 344 | paths := []string{} 345 | // a raid driver has directory entries for each of the scsi hosts on that controller. 346 | // $cd /sys/bus/pci/drivers/ 347 | // $ for d in *; do [ -d "$d" ] || continue; echo "$d -> $( cd "$d" && pwd -P )"; done 348 | // 0000:3c:00.0 -> /sys/devices/pci0000:3a/0000:3a:02.0/0000:3c:00.0 349 | // module -> /sys/module/ 350 | 351 | // We take a hack path and consider anything with a ":" in that dir as a host path. 352 | matches, err := filepath.Glob(driverSysPath + "/*:*") 353 | 354 | if err != nil { 355 | fmt.Printf("errors: %s\n", err) 356 | return paths 357 | } 358 | 359 | for _, p := range matches { 360 | fp, err := filepath.EvalSymlinks(p) 361 | 362 | if err == nil { 363 | paths = append(paths, fp) 364 | } 365 | } 366 | 367 | return paths 368 | } 369 | -------------------------------------------------------------------------------- /disk_test.go: -------------------------------------------------------------------------------- 1 | package disko_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | "testing" 9 | 10 | "machinerun.io/disko" 11 | "machinerun.io/disko/partid" 12 | ) 13 | 14 | func TestFreeSpaceSize(t *testing.T) { 15 | values := []struct{ start, last, expected uint64 }{ 16 | {0, 9, 10}, 17 | {0, 199, 200}, 18 | {100, 200, 101}, 19 | } 20 | 21 | for _, v := range values { 22 | f := disko.FreeSpace{v.start, v.last} 23 | found := f.Size() 24 | 25 | if v.expected != found { 26 | t.Errorf("Size(%v) expected %d found %d", 27 | f, v.expected, found) 28 | } 29 | } 30 | } 31 | 32 | func TestPartitionSize(t *testing.T) { 33 | tables := []struct{ start, last, expected uint64 }{ 34 | {0, 99, 100}, 35 | {3 * disko.Mebibyte, (5000+3)*disko.Mebibyte - 1, 5000 * disko.Mebibyte}, 36 | } 37 | 38 | for _, table := range tables { 39 | p := disko.Partition{Start: table.start, Last: table.last} 40 | found := p.Size() 41 | 42 | if table.expected != found { 43 | t.Errorf("Size(%v) expected %d found %d", p, table.expected, found) 44 | } 45 | } 46 | } 47 | 48 | func TestDiskString(t *testing.T) { 49 | mib := disko.Mebibyte 50 | gb := uint64(1000 * 1000 * 1000) 51 | 52 | d := disko.Disk{ 53 | Name: "sde", 54 | Path: "/dev/sde", 55 | Size: gb, 56 | SectorSize: 512, 57 | Type: disko.HDD, 58 | Attachment: disko.ATA, 59 | Partitions: disko.PartitionSet{ 60 | 1: disko.Partition{Start: 3 * mib, Last: 253*mib - 1, Number: 1}, 61 | 3: disko.Partition{Start: 500 * mib, Last: 600*mib - 1, Number: 3}, 62 | }, 63 | UdevInfo: disko.UdevInfo{}, 64 | } 65 | found := " " + d.String() + " " 66 | 67 | // disk size 1gb = 953 MiB. 600 = (253-3) + (953-600) 68 | expectedFree := 600 69 | 70 | for _, substr := range []string{ 71 | fmt.Sprintf("Size=%d", gb), 72 | fmt.Sprintf("FreeSpace=%dMiB/2", expectedFree), 73 | fmt.Sprintf("NumParts=%d", len(d.Partitions))} { 74 | if !strings.Contains(found, " "+substr+" ") { 75 | t.Errorf("%s: missing expected substring ' %s '", found, substr) 76 | } 77 | } 78 | } 79 | 80 | func TestDiskDetails(t *testing.T) { 81 | mib := disko.Mebibyte 82 | 83 | myType, err := disko.StringToGUID("9eb08654-de0e-4a63-967f-67a81d2ec0f0") 84 | if err != nil { 85 | t.Error(err) 86 | } 87 | 88 | d := disko.Disk{ 89 | Name: "sde", 90 | Path: "/dev/sde", 91 | Size: mib * mib, 92 | SectorSize: 512, 93 | Type: disko.HDD, 94 | Attachment: disko.ATA, 95 | Partitions: disko.PartitionSet{ 96 | 1: disko.Partition{Start: 3 * mib, Last: 253*mib - 1, Number: 1, 97 | Name: "my-name", Type: partid.LinuxLVM}, 98 | 2: disko.Partition{Start: 253 * mib, Last: 400*mib - 1, Number: 2, 99 | Type: disko.PartType(myType)}, 100 | }, 101 | UdevInfo: disko.UdevInfo{}, 102 | } 103 | expected := ` 104 | [ # Start Last Size Name Type ] 105 | [ 1 3 MiB 253 MiB 250 MiB my-name LVM ] 106 | [ 2 253 MiB 400 MiB 147 MiB N/A 9EB08654-DE0E-4A63-967F-67A81D2EC0F0 ] 107 | [ - 400 MiB 1048575 MiB 1048175 MiB N/A ] 108 | ` 109 | 110 | spaces := regexp.MustCompile("[ ]+") 111 | found := strings.TrimSpace(spaces.ReplaceAllString(d.Details(), " ")) 112 | expShort := strings.TrimSpace(spaces.ReplaceAllString(expected, " ")) 113 | 114 | if expShort != found { 115 | t.Errorf("Expected: '%s'\nFound: '%s'\n", expShort, found) 116 | } 117 | } 118 | 119 | func TestDiskTypeString(t *testing.T) { 120 | for _, d := range []struct { 121 | dtype disko.DiskType 122 | expected string 123 | }{ 124 | {disko.HDD, "HDD"}, 125 | {disko.SSD, "SSD"}, 126 | {disko.NVME, "NVME"}, 127 | } { 128 | found := d.dtype.String() 129 | if found != d.expected { 130 | t.Errorf("disko.DiskType(%d).String() found %s, expected %s", 131 | d.dtype, found, d.expected) 132 | } 133 | } 134 | } 135 | 136 | func TestAttachmentTypeString(t *testing.T) { 137 | for _, d := range []struct { 138 | dtype disko.AttachmentType 139 | expected string 140 | }{ 141 | {disko.UnknownAttach, "UNKNOWN"}, 142 | {disko.RAID, "RAID"}, 143 | {disko.SCSI, "SCSI"}, 144 | {disko.ATA, "ATA"}, 145 | {disko.PCIE, "PCIE"}, 146 | {disko.USB, "USB"}, 147 | {disko.VIRTIO, "VIRTIO"}, 148 | {disko.IDE, "IDE"}, 149 | {disko.NBD, "NBD"}, 150 | {disko.LOOP, "LOOP"}, 151 | } { 152 | found := d.dtype.String() 153 | if found != d.expected { 154 | t.Errorf("disko.AttachmentType(%d).String() found %s, expected %s", 155 | d.dtype, found, d.expected) 156 | } 157 | } 158 | } 159 | 160 | func TestPartitionSerializeJson(t *testing.T) { 161 | // For readability, Partition serializes ID and Type to string GUIDs 162 | // Test that they get there. 163 | myIDStr := "01234567-89AB-CDEF-0123-456789ABCDEF" 164 | myID, _ := disko.StringToGUID(myIDStr) 165 | p := disko.Partition{ 166 | Start: 3 * disko.Mebibyte, 167 | Last: 253*disko.Mebibyte - 1, 168 | ID: myID, 169 | Type: partid.EFI, 170 | Name: "my system part", 171 | Number: 1, 172 | } 173 | 174 | jbytes, err := json.MarshalIndent(&p, "", " ") 175 | if err != nil { 176 | t.Errorf("Failed to marshal %#v: %s", p, err) 177 | } 178 | 179 | jstr := string(jbytes) 180 | if !strings.Contains(jstr, myIDStr) { 181 | t.Errorf("Did not find string ID '%s' in json: %s", myIDStr, jstr) 182 | } 183 | 184 | typeStr := disko.GUIDToString(disko.GUID(partid.EFI)) 185 | if !strings.Contains(jstr, typeStr) { 186 | t.Errorf("Did not find string Type '%s' in json: %s", myIDStr, jstr) 187 | } 188 | 189 | fmt.Printf("%s\n", jstr) 190 | } 191 | 192 | func TestPartitionUnserializeJson(t *testing.T) { 193 | myIDStr := "01234567-89AB-CDEF-0123-456789ABCDEF" 194 | myID, _ := disko.StringToGUID(myIDStr) 195 | jbytes := []byte(`{ 196 | "start": 3145728, 197 | "last": 265289727, 198 | "id": "01234567-89AB-CDEF-0123-456789ABCDEF", 199 | "type": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", 200 | "name": "my system part", 201 | "number": 1 202 | }`) 203 | 204 | found := disko.Partition{} 205 | 206 | err := json.Unmarshal(jbytes, &found) 207 | if err != nil { 208 | t.Errorf("Failed Unmarshal of bytes to Partition: %s", err) 209 | } 210 | 211 | expected := disko.Partition{ 212 | Start: 3 * disko.Mebibyte, 213 | Last: 253*disko.Mebibyte - 1, 214 | ID: myID, 215 | Type: partid.EFI, 216 | Name: "my system part", 217 | Number: 1, 218 | } 219 | 220 | if expected != found { 221 | t.Errorf("Objects differed. got %#v expected %#v\n", found, expected) 222 | } 223 | } 224 | 225 | func TestDiskSerializeJson(t *testing.T) { 226 | // For readability, Partition serializes ID and Type to string GUIDs 227 | // Test that they get there. 228 | d := disko.Disk{ 229 | Name: "sda", 230 | Path: "/dev/sda", 231 | Size: 500 * disko.Mebibyte, 232 | SectorSize: 512, 233 | Type: disko.HDD, 234 | Attachment: disko.ATA, 235 | } 236 | 237 | jbytes, err := json.MarshalIndent(&d, "", " ") 238 | if err != nil { 239 | t.Errorf("Failed to marshal %#v: %s", d, err) 240 | } 241 | 242 | jstr := string(jbytes) 243 | if !strings.Contains(jstr, "HDD") { 244 | t.Errorf("Did not find string 'HDD' in json: %s", jstr) 245 | } 246 | } 247 | 248 | func compareDisk(a *disko.Disk, b *disko.Disk) bool { 249 | return (a.Name == b.Name && 250 | a.Path == b.Path && 251 | a.Size == b.Size && 252 | a.SectorSize == b.SectorSize && 253 | a.Type == b.Type && 254 | a.Attachment == b.Attachment) 255 | } 256 | 257 | func TestDiskUnserializeJson(t *testing.T) { 258 | expected := disko.Disk{ 259 | Name: "sda", 260 | Path: "/dev/sda", 261 | Size: 500 * disko.Mebibyte, 262 | SectorSize: 512, 263 | Type: disko.HDD, 264 | Attachment: disko.ATA, 265 | } 266 | 267 | for _, jbytes := range [][]byte{ 268 | []byte(`{ 269 | "name": "sda", 270 | "path": "/dev/sda", 271 | "size": 524288000, 272 | "sectorSize": 512, 273 | "type": "HDD", 274 | "attachment": "ATA"}`), 275 | []byte(`{ 276 | "name": "sda", 277 | "path": "/dev/sda", 278 | "size": 524288000, 279 | "sectorSize": 512, 280 | "type": 0, 281 | "attachment": 3}`)} { 282 | found := disko.Disk{} 283 | err := json.Unmarshal(jbytes, &found) 284 | 285 | if err != nil { 286 | t.Errorf("Failed Unmarshal of bytes to Disk: %s", err) 287 | } 288 | 289 | if !compareDisk(&found, &expected) { 290 | t.Errorf("Objects differed. got %#v expected %#v\n", found, expected) 291 | } 292 | } 293 | } 294 | 295 | func checkPropertySetEqual(a, b disko.PropertySet) bool { 296 | if len(a) != len(b) { 297 | return false 298 | } 299 | 300 | for k, v1 := range a { 301 | if v2, ok := b[k]; ok != true || v1 != v2 { 302 | return false 303 | } 304 | } 305 | 306 | for k, v1 := range b { 307 | if v2, ok := a[k]; ok != true || v1 != v2 { 308 | return false 309 | } 310 | } 311 | 312 | return true 313 | } 314 | 315 | // Unmarshal either a list of strings or a PropertySet. 316 | func TestUnmarshalProperties(t *testing.T) { 317 | tables := []struct { 318 | input string 319 | expected disko.PropertySet 320 | msg string 321 | }{ 322 | { 323 | `["EPHEMERAL"]`, 324 | disko.PropertySet{disko.Ephemeral: true}, 325 | "simple test", 326 | }, 327 | { 328 | `{"EPHEMERAL": true}`, 329 | disko.PropertySet{disko.Ephemeral: true}, 330 | "map string:bool supported.", 331 | }, 332 | { 333 | `{"PROP1": true, "PROP2": false}`, 334 | disko.PropertySet{disko.Property("PROP1"): true}, 335 | "false values dropped.", 336 | }, 337 | } 338 | 339 | for _, table := range tables { 340 | found := disko.PropertySet{} 341 | 342 | err := found.UnmarshalJSON([]byte(table.input)) 343 | if err != nil { 344 | t.Errorf("UnmarshalJSON(%s) returned error %s", table.input, err) 345 | continue 346 | } 347 | 348 | if !checkPropertySetEqual(found, table.expected) { 349 | t.Errorf("UnmarshalJSON(%s) returned %#v. expected %#v (%s)", 350 | table.input, found, table.expected, table.msg) 351 | } 352 | 353 | fmt.Printf("%s: found %#v\n", table.msg, table.expected) 354 | } 355 | } 356 | 357 | // PropertySet should marshal into a sorted list of strings. 358 | func TestMarshalProperties(t *testing.T) { 359 | tables := []struct { 360 | input disko.PropertySet 361 | expected string 362 | msg string 363 | }{ 364 | { 365 | disko.PropertySet{disko.Ephemeral: true}, 366 | `["EPHEMERAL"]`, 367 | "simple test", 368 | }, 369 | { 370 | disko.PropertySet{disko.Ephemeral: false, disko.Property("SILLY"): true}, 371 | `["SILLY"]`, 372 | "false values are not included", 373 | }, 374 | { 375 | disko.PropertySet{ 376 | disko.Property("ARTSY"): true, 377 | disko.Ephemeral: true, 378 | disko.Property("SILLY"): true}, 379 | `["ARTSY","EPHEMERAL","SILLY"]`, 380 | "values are sorted", 381 | }, 382 | } 383 | 384 | for _, table := range tables { 385 | found, err := table.input.MarshalJSON() 386 | if err != nil { 387 | t.Errorf("MarshalJSON(%v) returned error %s", table.input, err) 388 | continue 389 | } 390 | 391 | if string(found) != table.expected { 392 | t.Errorf("MarshalJSON(%v) returned %v. expected %v", table.input, 393 | string(found), table.expected) 394 | } 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /mpi3mr/storcli2_test.go: -------------------------------------------------------------------------------- 1 | package mpi3mr 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | ) 9 | 10 | var showNoLogJData = ` 11 | { 12 | "Controllers": [ 13 | { 14 | "Command Status": { "CLI Version": "008.0011.0000.0014 Sep 26, 2024", "Operating system": "Linux6.11.0-8-generic", 15 | "Status Code": 0, 16 | "Status": "Success", 17 | "Description": "None" 18 | }, 19 | "Response Data": { 20 | "Number of Controllers": 1, 21 | "Host Name": "ubuntu-server", 22 | "Operating System ": "Linux6.11.0-8-generic", 23 | "SL8 Library Version": "08.1113.0000", 24 | "System Overview": [ 25 | { 26 | "Ctrl": 0, 27 | "Product Name": "Cisco Tri-Mode 24G SAS RAID Controller w/4GB Cache", 28 | "SASAddress": "0X52CF89BD43A7AF80", 29 | "Personality": "RAID", 30 | "Status": "Optimal", 31 | "PD(s)": 1, 32 | "VD(s)": 1, 33 | "VNOpt": 0, 34 | "EPack": "Optimal", 35 | "SerialNumber": "SPD5001205 " 36 | } 37 | ] 38 | } 39 | } 40 | ] 41 | } 42 | ` 43 | 44 | func TestStorCli2CmdShow(t *testing.T) { 45 | controllers, err := parseShowOutput([]byte(showNoLogJData)) 46 | if err != nil { 47 | t.Fatalf("Failed to parse show output: %v", err) 48 | } 49 | 50 | if len(controllers) != 1 { 51 | t.Fatalf("Expected '1' controller, got %d", len(controllers)) 52 | } 53 | } 54 | 55 | var showC0ShowNoLogJData = ` 56 | { 57 | "Controllers": [ 58 | { 59 | "Command Status": { 60 | "CLI Version": "008.0011.0000.0014 Sep 26, 2024", 61 | "Operating system": "Linux6.11.0-8-generic", 62 | "Controller": "0", 63 | "Status": "Success", 64 | "Description": "None" 65 | }, 66 | "Response Data": { 67 | "Product Name": "Cisco Tri-Mode 24G SAS RAID Controller w/4GB Cache", 68 | "Board Name": "UCSC-RAID-HP ", 69 | "Board Assembly": "03-50146-00006 ", 70 | "Board Tracer Number": "SPD5001205 ", 71 | "Board Revision": "00006 ", 72 | "Chip Name": "SAS4116W ", 73 | "Chip Revision": "B0 ", 74 | "Package Version": "8.6.2.0-00065-00001", 75 | "Firmware Version": "8.6.2.0-00000-00001", 76 | "Firmware Security Version Number": "00.00.00.00", 77 | "NVDATA Version": "06.0B.00.0D", 78 | "Driver Name": "mpi3mr", 79 | "Driver Version": "8.9.1.0.51", 80 | "SAS Address": "0x52cf89bd43a7af80", 81 | "Serial Number": "SPD5001205 ", 82 | "Controller Time(LocalTime yyyy/mm/dd hh:mm:sec)": "2025/02/03 22:50:27", 83 | "System Time(LocalTime yyyy/mm/dd hh:mm:sec)": "2025/02/03 22:50:27", 84 | "Board Mfg Date(yyyy/mm/dd)": "2023/12/20", 85 | "Controller Personality": "RAID", 86 | "Max PCIe Link Rate": "0x08 (16GT/s)", 87 | "Max PCIe Port Width": 16, 88 | "PCI Address": "00:01:00:0", 89 | "PCIe Link Width": "X16 Lane(s)", 90 | "Current Max PCI Link Speed": "16GT/s", 91 | "Current PCIe Port Width": 16, 92 | "SAS/SATA": "SAS/SATA-6G, SAS-12G, SAS-22.5G", 93 | "PCIe": "PCIE-2.5GT, PCIE-5GT, PCIE-8GT, PCIE-16GT", 94 | "PCI Vendor ID": "0x1000", 95 | "PCI Device ID": "0x00A5", 96 | "PCI Subsystem Vendor ID": "0x1137", 97 | "PCI Subsystem ID": "0x2EB", 98 | "Security Protocol": "SPDM-1.1.0,1.0.0", 99 | "PCI Slot Number": 11, 100 | "Drive Groups": 1, 101 | "TOPOLOGY": [ 102 | { 103 | "DG": 0, 104 | "Span": "-", 105 | "Row": "-", 106 | "EID:Slot": "-", 107 | "PID": "-", 108 | "Type": "RAID0", 109 | "State": "-", 110 | "Status": "-", 111 | "BT": "N", 112 | "Size": "893.137 GiB", 113 | "PDC": "dflt", 114 | "Secured": "N", 115 | "FSpace": "N" 116 | }, 117 | { 118 | "DG": 0, 119 | "Span": 0, 120 | "Row": "-", 121 | "EID:Slot": "-", 122 | "PID": "-", 123 | "Type": "RAID0", 124 | "State": "-", 125 | "Status": "-", 126 | "BT": "N", 127 | "Size": "893.137 GiB", 128 | "PDC": "dflt", 129 | "Secured": "N", 130 | "FSpace": "N" 131 | }, 132 | { 133 | "DG": 0, 134 | "Span": 0, 135 | "Row": 0, 136 | "EID:Slot": "322:2", 137 | "PID": 312, 138 | "Type": "DRIVE", 139 | "State": "Conf", 140 | "Status": "Online", 141 | "BT": "N", 142 | "Size": "893.137 GiB", 143 | "PDC": "dflt", 144 | "Secured": "N", 145 | "FSpace": "-" 146 | } 147 | ], 148 | "Virtual Drives": 1, 149 | "VD LIST": [ 150 | { 151 | "DG/VD": "0/1", 152 | "TYPE": "RAID0", 153 | "State": "Optl", 154 | "Access": "RW", 155 | "CurrentCache": "NR,WB", 156 | "DefaultCache": "NR,WB", 157 | "Size": "893.137 GiB", 158 | "Name": "" 159 | } 160 | ], 161 | "Physical Drives": 1, 162 | "PD LIST": [ 163 | { 164 | "EID:Slt": "322:2", 165 | "PID": 312, 166 | "State": "Conf", 167 | "Status": "Online", 168 | "DG": 0, 169 | "Size": "893.137 GiB", 170 | "Intf": "SATA", 171 | "Med": "SSD", 172 | "SED_Type": "-", 173 | "SeSz": "512B", 174 | "Model": "SAMSUNG MZ7L3960HCJR-00AK1", 175 | "Sp": "U", 176 | "LU/NS Count": 1, 177 | "Alt-EID": "-" 178 | } 179 | ], 180 | "LU/NS LIST": [ 181 | { 182 | "PID": 312, 183 | "LUN/NSID": "0/-", 184 | "Index": 255, 185 | "Status": "Online", 186 | "Size": "893.137 GiB" 187 | } 188 | ], 189 | "Enclosures": 1, 190 | "Enclosure List": [ 191 | { 192 | "EID": 322, 193 | "State": "OK", 194 | "DeviceType": "Logical Enclosure", 195 | "Slots": 10, 196 | "PD": 1, 197 | "Partner-EID": "-", 198 | "Multipath": "No", 199 | "PS": 0, 200 | "Fans": 0, 201 | "TSs": 0, 202 | "Alms": 0, 203 | "SIM": 0, 204 | "ProdID": "VirtualSES " 205 | } 206 | ], 207 | "Energy Pack Info": [ 208 | { 209 | "Type": "Supercap", 210 | "SubType": "FBU345", 211 | "Voltage(mV)": 8937, 212 | "Temperature(C)": 23, 213 | "Status": "Optimal" 214 | } 215 | ] 216 | } 217 | } 218 | ] 219 | } 220 | ` 221 | 222 | var testc0VAllShowAllJOut = ` 223 | { 224 | "Controllers": [ 225 | { 226 | "Command Status": { 227 | "CLI Version": "008.0011.0000.0014 Sep 26, 2024", 228 | "Operating system": "Linux6.11.0-8-generic", 229 | "Controller": "0", 230 | "Status": "Success", 231 | "Description": "None" 232 | }, 233 | "Response Data": { 234 | "Virtual Drives": [ 235 | { 236 | "VD Info": { 237 | "DG/VD": "0/1", 238 | "TYPE": "RAID0", 239 | "State": "Optl", 240 | "Access": "RW", 241 | "CurrentCache": "NR,WB", 242 | "DefaultCache": "NR,WB", 243 | "Size": "893.137 GiB", 244 | "Name": "" 245 | }, 246 | "PDs": [ 247 | { 248 | "EID:Slt": "322:2", 249 | "PID": 312, 250 | "State": "Conf", 251 | "Status": "Online", 252 | "DG": 0, 253 | "Size": "893.137 GiB", 254 | "Intf": "SATA", 255 | "Med": "SSD", 256 | "SED_Type": "-", 257 | "SeSz": "512B", 258 | "Model": "SAMSUNG MZ7L3960HCJR-00AK1", 259 | "Sp": "U", 260 | "LU/NS Count": 1, 261 | "Alt-EID": "-" 262 | } 263 | ], 264 | "VD Properties": { 265 | "Strip Size": "64 KiB", 266 | "Block Size": 4096, 267 | "Number of Blocks": 234130688, 268 | "Span Depth": 1, 269 | "Number of Drives": 1, 270 | "Drive Write Cache Policy": "Default", 271 | "Default Power Save Policy": "Default", 272 | "Current Power Save Policy": "Default", 273 | "Access Policy Status": "VD has desired access", 274 | "Auto BGI": "Off", 275 | "Secured": "No", 276 | "Init State": "No Init", 277 | "Consistent": "Yes", 278 | "Morphing": "No", 279 | "Cache Preserved": "No", 280 | "Bad Block Exists": "No", 281 | "VD Ready for OS Requests": "Yes", 282 | "Reached LD BBM failure threshold": "No", 283 | "Supported Erase Types": "Simple, Normal, Thorough", 284 | "Exposed to OS": "Yes", 285 | "Creation Time(LocalTime yyyy/mm/dd hh:mm:sec)": "2025/01/29 22:07:51", 286 | "Default Cachebypass Mode": "Cachebypass Not Performed For Any IOs", 287 | "Current Cachebypass Mode": "Cachebypass Not Performed For Any IOs", 288 | "SCSI NAA Id": "62cf89bd43a7af80679aa6b751349581", 289 | "OS Drive Name": "/dev/sdg", 290 | "Current Unmap Status": "No", 291 | "Current WriteSame Unmap Status": "No", 292 | "LU/NS Count used per PD": 1, 293 | "Data Format for I/O": "None", 294 | "Serial Number": "0081953451b7a69a6780afa743bd89cf" 295 | } 296 | } 297 | ] 298 | } 299 | } 300 | ] 301 | } 302 | ` 303 | var expectedControllerBytes = ` 304 | { 305 | "Controller": { 306 | "ID": 0, 307 | "PhysicalDrives": { 308 | "0": { 309 | "EID:Slt": "322:2", 310 | "PID": 0, 311 | "State": "Conf", 312 | "Status": "Online", 313 | "DG": 0, 314 | "Size": "893.137 GiB", 315 | "Intf": "SATA", 316 | "Med": "SSD", 317 | "SED_Type": "-", 318 | "SeSz": "512B", 319 | "Model": "SAMSUNG MZ7L3960HCJR-00AK1", 320 | "Sp": "U", 321 | "LU/NS Count": 1, 322 | "Alt-EID": "-" 323 | } 324 | }, 325 | "VirtualDrives": { 326 | "0/1": { 327 | "DG/VD": "0/1", 328 | "TYPE": "RAID0", 329 | "State": "Optl", 330 | "Access": "RW", 331 | "CurrentCache": "NR,WB", 332 | "DefaultCache": "NR,WB", 333 | "Size": "893.137 GiB", 334 | "Properties": { 335 | "Strip Size": "64 KiB", 336 | "Block Size": 4096, 337 | "Number of Blocks": 234130688, 338 | "Span Depth": 1, 339 | "Number of Drives": 1, 340 | "Drive Write Cache Policy": "Default", 341 | "Default Power Save Policy": "Default", 342 | "Current Power Save Policy": "Default", 343 | "Access Policy Status": "VD has desired access", 344 | "Auto BGI": "Off", 345 | "Secured": "No", 346 | "Init State": "No Init", 347 | "Consistent": "Yes", 348 | "Morphing": "No", 349 | "Cache Preserved": "No", 350 | "Bad Block Exists": "No", 351 | "VD Ready for OS Requests": "Yes", 352 | "Reached LD BBM failure threshold": "No", 353 | "Supported Erase Types": "Simple, Normal, Thorough", 354 | "Exposed to OS": "Yes", 355 | "Creation Time(LocalTime yyyy/mm/dd hh:mm:sec)": "2025/01/29 22:07:51", 356 | "Default Cachebypass Mode": "Cachebypass Not Performed For Any IOs", 357 | "Current Cachebypass Mode": "Cachebypass Not Performed For Any IOs", 358 | "SCSI NAA Id": "62cf89bd43a7af80679aa6b751349581", 359 | "OS Drive Name": "/dev/sdg", 360 | "Current Unmap Status": "No", 361 | "Current WriteSame Unmap Status": "No", 362 | "LU/NS Count used per PD": 1, 363 | "Data Format for I/O": "None", 364 | "Serial Number": "0081953451b7a69a6780afa743bd89cf" 365 | }, 366 | "PhysicalDrives": [ 367 | { 368 | "EID:Slt": "322:2", 369 | "PID": 0, 370 | "State": "Conf", 371 | "Status": "Online", 372 | "DG": 0, 373 | "Size": "893.137 GiB", 374 | "Intf": "SATA", 375 | "Med": "SSD", 376 | "SED_Type": "-", 377 | "SeSz": "512B", 378 | "Model": "SAMSUNG MZ7L3960HCJR-00AK1", 379 | "Sp": "U", 380 | "LU/NS Count": 1, 381 | "Alt-EID": "-" 382 | } 383 | ] 384 | } 385 | } 386 | } 387 | } 388 | ` 389 | 390 | func TestStorCli2NewController(t *testing.T) { 391 | ctrlID := 0 392 | 393 | cxShowNoLogJOut := []byte(showC0ShowNoLogJData) 394 | cxVallShowAllNoLogJOut := []byte(testc0VAllShowAllJOut) 395 | 396 | got, err := newController(ctrlID, cxShowNoLogJOut, cxVallShowAllNoLogJOut) 397 | if err != nil { 398 | t.Fatalf("failed to create new controller") 399 | } 400 | 401 | // we wrap the controller in a struct so we can capture the full struture in JSON 402 | type Payload struct { 403 | Controller Controller `json:"Controller"` 404 | } 405 | p := Payload{} 406 | p.Controller = got 407 | 408 | // Load the expectec controller 409 | want := Payload{} 410 | if err := json.Unmarshal([]byte(expectedControllerBytes), &want); err != nil { 411 | t.Fatalf("Failed to unmarshal expected controller: %s", err) 412 | } 413 | 414 | if diff := cmp.Diff(p, want); diff != "" { 415 | t.Errorf("got:\n%+v\nwant:\n+%v", got, want) 416 | } 417 | } 418 | 419 | var pdForeignData = ` 420 | { 421 | "EID:Slt": "322:5", 422 | "PID": 315, 423 | "State": "UConf", 424 | "Status": "Good", 425 | "DG": "F", 426 | "Size": "1.090 TiB", 427 | "Intf": "SAS", 428 | "Med": "HDD", 429 | "SED_Type": "-", 430 | "SeSz": "512B", 431 | "Model": "AL15SEB120N ", 432 | "Sp": "U", 433 | "LU/NS Count": 1, 434 | "Alt-EID": "-" 435 | } 436 | ` 437 | 438 | func TestStorCli2ParsePhysicalDriveForeign(t *testing.T) { 439 | pd := PhysicalDrive{} 440 | if err := json.Unmarshal([]byte(pdForeignData), &pd); err != nil { 441 | t.Fatalf("failed to unmarshal PhysicalDrive with Foreign 'DG': %v", err) 442 | } 443 | 444 | if pd.DG != DriveDGForeign { 445 | t.Fatalf("expected DG %d, got DG %d", DriveDGForeign, pd.DG) 446 | } 447 | 448 | } 449 | -------------------------------------------------------------------------------- /disk.go: -------------------------------------------------------------------------------- 1 | package disko 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | 9 | "machinerun.io/disko/partid" 10 | ) 11 | 12 | // DiskType enumerates supported disk types. 13 | type DiskType int 14 | 15 | const ( 16 | // HDD - hard disk drive 17 | HDD DiskType = iota 18 | 19 | // SSD - solid state disk 20 | SSD 21 | 22 | // NVME - Non-volatile memory express 23 | NVME 24 | 25 | // TYPEFILE - A file on disk, not a block device. 26 | TYPEFILE 27 | ) 28 | 29 | func (t DiskType) String() string { 30 | return []string{"HDD", "SSD", "NVME", "FILE"}[t] 31 | } 32 | 33 | // StringToDiskType - convert a string to a disk type. 34 | func StringToDiskType(typeStr string) DiskType { 35 | kmap := map[string]DiskType{ 36 | "HDD": HDD, 37 | "SSD": SSD, 38 | "NVME": NVME, 39 | "FILE": TYPEFILE, 40 | } 41 | if dtype, ok := kmap[typeStr]; ok { 42 | return dtype 43 | } 44 | 45 | return HDD 46 | } 47 | 48 | // MarshalJSON - Custom to marshal as a string. 49 | func (t DiskType) MarshalJSON() ([]byte, error) { 50 | return json.Marshal(t.String()) 51 | } 52 | 53 | // UnmarshalJSON - custom to read as string or int. 54 | func (t *DiskType) UnmarshalJSON(b []byte) error { 55 | var err error 56 | var asStr string 57 | var asInt int 58 | 59 | err = json.Unmarshal(b, &asInt) 60 | if err == nil { 61 | *t = DiskType(asInt) 62 | return nil 63 | } 64 | 65 | err = json.Unmarshal(b, &asStr) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | dtype := StringToDiskType(asStr) 71 | *t = dtype 72 | 73 | return nil 74 | } 75 | 76 | // AttachmentType enumerates the type of device to which the disks are 77 | // attached to in the system. 78 | type AttachmentType int 79 | 80 | const ( 81 | // UnknownAttach - indicates an unknown attachment. 82 | UnknownAttach AttachmentType = iota 83 | 84 | // RAID - indicates that the device is attached to RAID card 85 | RAID 86 | 87 | // SCSI - indicates device is attached to scsi, but not a RAID card. 88 | SCSI 89 | 90 | // ATA - indicates that the device is attached to ATA card 91 | ATA 92 | 93 | // PCIE - indicates that the device is attached to PCIE card 94 | PCIE 95 | 96 | // USB - indicates that the device is attached to USB bus 97 | USB 98 | 99 | // VIRTIO - indicates that the device is attached to virtio. 100 | VIRTIO 101 | 102 | // IDE - indicates that the device is attached to IDE. 103 | IDE 104 | 105 | // NBD - a network block device (/dev/nbd0) 106 | NBD 107 | 108 | // LOOP - a loop device (/dev/loop0) 109 | LOOP 110 | 111 | // XENBUS - xen blkfront 112 | XENBUS 113 | 114 | // FILESYSTEM - a file on a filesystem. 115 | FILESYSTEM 116 | ) 117 | 118 | func (t AttachmentType) String() string { 119 | return []string{"UNKNOWN", "RAID", "SCSI", "ATA", "PCIE", "USB", 120 | "VIRTIO", "IDE", "NBD", "LOOP", "XENBUS", "FILESYSTEM"}[t] 121 | } 122 | 123 | // StringToAttachmentType - Convert a string to an AttachmentType 124 | func StringToAttachmentType(atypeStr string) AttachmentType { 125 | kmap := map[string]AttachmentType{ 126 | "UNKNOWN": UnknownAttach, 127 | "RAID": RAID, 128 | "SCSI": SCSI, 129 | "ATA": ATA, 130 | "PCIE": PCIE, 131 | "VIRTIO": VIRTIO, 132 | "IDE": IDE, 133 | "NBD": NBD, 134 | "LOOP": LOOP, 135 | "XENBUS": XENBUS, 136 | "FILESYSTEM": FILESYSTEM, 137 | } 138 | 139 | if atype, ok := kmap[atypeStr]; ok { 140 | return atype 141 | } 142 | 143 | return UnknownAttach 144 | } 145 | 146 | // MarshalJSON - Custom to marshal as a string. 147 | func (t AttachmentType) MarshalJSON() ([]byte, error) { 148 | return json.Marshal(t.String()) 149 | } 150 | 151 | // UnmarshalJSON - reverse of the custom marshler 152 | func (t *AttachmentType) UnmarshalJSON(b []byte) error { 153 | var err error 154 | var asStr string 155 | var asInt int 156 | 157 | err = json.Unmarshal(b, &asInt) 158 | if err == nil { 159 | *t = AttachmentType(asInt) 160 | return nil 161 | } 162 | 163 | err = json.Unmarshal(b, &asStr) 164 | if err != nil { 165 | return err 166 | } 167 | 168 | dtype := StringToAttachmentType(asStr) 169 | *t = dtype 170 | 171 | return nil 172 | } 173 | 174 | // TableType enumerates the type of device to which the disks are 175 | // attached to in the system. 176 | type TableType int 177 | 178 | const ( 179 | // TableNone - no table type 180 | TableNone TableType = iota 181 | 182 | // MBR - a Master Boot Record style partition table. 183 | MBR 184 | 185 | // GPT - a Guid Partition Table style partition table 186 | GPT 187 | ) 188 | 189 | func (t TableType) String() string { 190 | return []string{"NONE", "MBR", "GPT"}[t] 191 | } 192 | 193 | // StringToTableType - Convert a string to an TableType 194 | func StringToTableType(atypeStr string) TableType { 195 | kmap := map[string]TableType{ 196 | "NONE": TableNone, 197 | "MBR": MBR, 198 | "GPT": GPT, 199 | } 200 | 201 | if atype, ok := kmap[atypeStr]; ok { 202 | return atype 203 | } 204 | 205 | return TableNone 206 | } 207 | 208 | // MarshalJSON - Custom to marshal as a string. 209 | func (t TableType) MarshalJSON() ([]byte, error) { 210 | return json.Marshal(t.String()) 211 | } 212 | 213 | // UnmarshalJSON - reverse of the custom marshler 214 | func (t *TableType) UnmarshalJSON(b []byte) error { 215 | var err error 216 | var asStr string 217 | var asInt int 218 | 219 | err = json.Unmarshal(b, &asInt) 220 | if err == nil { 221 | *t = TableType(asInt) 222 | return nil 223 | } 224 | 225 | err = json.Unmarshal(b, &asStr) 226 | if err != nil { 227 | return err 228 | } 229 | 230 | dtype := StringToTableType(asStr) 231 | *t = dtype 232 | 233 | return nil 234 | } 235 | 236 | // PartType represents a GPT Partition GUID 237 | type PartType GUID 238 | 239 | // DiskSet is a map of the kernel device name and the disk. 240 | type DiskSet map[string]Disk 241 | 242 | // Details prints the details of the disks in the disk set ina a tabular 243 | // format. 244 | func (ds DiskSet) Details() string { 245 | return "" 246 | } 247 | 248 | // Property - a property of a disk such 249 | type Property string 250 | 251 | const ( 252 | // Ephemeral - A cloud ephemeral disk. 253 | Ephemeral Property = "EPHEMERAL" 254 | ) 255 | 256 | // PropertySet - a group of properties of a disk 257 | type PropertySet map[Property]bool 258 | 259 | // MarshalJSON - serialize to json 260 | func (p PropertySet) MarshalJSON() ([]byte, error) { 261 | keys := []string{} 262 | 263 | for k := range p { 264 | // Drop false values. 265 | if !p[k] { 266 | continue 267 | } 268 | 269 | keys = append(keys, string(k)) 270 | } 271 | 272 | sort.Strings(keys) 273 | 274 | return json.Marshal(keys) 275 | } 276 | 277 | func (p PropertySet) String() string { 278 | keys := []string{} 279 | for k := range p { 280 | keys = append(keys, string(k)) 281 | } 282 | 283 | sort.Strings(keys) 284 | 285 | return strings.Join(keys, ",") 286 | } 287 | 288 | // UnmarshalJSON - json unserialize 289 | func (p *PropertySet) UnmarshalJSON(b []byte) error { 290 | s := map[string]bool{} 291 | 292 | err := json.Unmarshal(b, &s) 293 | if err == nil { 294 | for k, v := range s { 295 | // drop false values 296 | if !v { 297 | continue 298 | } 299 | 300 | (*p)[Property(k)] = v 301 | } 302 | 303 | return nil 304 | } 305 | 306 | slist := []string{} 307 | 308 | err = json.Unmarshal(b, &slist) 309 | if err != nil { 310 | return err 311 | } 312 | 313 | for _, k := range slist { 314 | (*p)[Property(k)] = true 315 | } 316 | 317 | return nil 318 | } 319 | 320 | // Disk wraps the disk level operations. It provides basic information 321 | // about the disk including name, device path, size etc. 322 | type Disk struct { 323 | // Name is the kernel name of the disk. 324 | Name string `json:"name"` 325 | 326 | // Path is the device path of the disk. 327 | Path string `json:"path"` 328 | 329 | // Size is the size of the disk in bytes. 330 | Size uint64 `json:"size"` 331 | 332 | // SectorSize is the sector size of the device, if its unknown or not 333 | // applicable it will return 0. 334 | SectorSize uint `json:"sectorSize"` 335 | 336 | // ReadOnly - cannot be written to. 337 | ReadOnly bool `json:"read-only"` 338 | 339 | // Type is the DiskType indicating the type of this disk. This value 340 | // can be used to determine if the disk is of a particular media type like 341 | // HDD, SSD or NVMe. 342 | Type DiskType `json:"type"` 343 | 344 | // Attachment is the type of storage card this disk is attached to. 345 | // For example: RAID, ATA or PCIE. 346 | Attachment AttachmentType `json:"attachment"` 347 | 348 | // Partitions is the set of partitions on this disk. 349 | Partitions PartitionSet `json:"partitions"` 350 | 351 | // TableType is the type of the table 352 | Table TableType `json:"table"` 353 | 354 | // Properties are a set of properties of this disk. 355 | Properties PropertySet `json:"properties"` 356 | 357 | // UdevInfo is the disk's udev information. 358 | UdevInfo UdevInfo `json:"udevInfo"` 359 | } 360 | 361 | // FreeSpacesWithMin returns a list of freespaces that are minSize long or more. 362 | func (d *Disk) FreeSpacesWithMin(minSize uint64) []FreeSpace { 363 | // Stay out of the first 1Mebibyte 364 | // Leave 33 sectors at end (for GPT second header) and round 1MiB down. 365 | end := ((d.Size - uint64(d.SectorSize)*33) / Mebibyte) * Mebibyte 366 | used := uRanges{{0, 1*Mebibyte - 1}, {end, d.Size}} 367 | 368 | for _, p := range d.Partitions { 369 | used = append(used, uRange{p.Start, p.Last}) 370 | } 371 | 372 | avail := []FreeSpace{} 373 | 374 | for _, g := range findRangeGaps(used, 0, d.Size) { 375 | if g.Size() < minSize { 376 | continue 377 | } 378 | 379 | avail = append(avail, FreeSpace(g)) 380 | } 381 | 382 | return avail 383 | } 384 | 385 | // FreeSpaces returns a list of slots of free spaces on the disk. These slots can 386 | // be used to create new partitions. 387 | func (d *Disk) FreeSpaces() []FreeSpace { 388 | return d.FreeSpacesWithMin(ExtentSize) 389 | } 390 | 391 | func (d Disk) String() string { 392 | var avail uint64 393 | 394 | fs := d.FreeSpaces() 395 | 396 | for _, f := range fs { 397 | avail += f.Size() 398 | } 399 | 400 | mbsize := func(n uint64) string { 401 | if (n)%Mebibyte == 0 { 402 | return fmt.Sprintf("%dMiB", (n)/Mebibyte) 403 | } 404 | 405 | return fmt.Sprintf("%d", n) 406 | } 407 | 408 | return fmt.Sprintf( 409 | ("%s (%s) Table=%s Size=%s NumParts=%d FreeSpace=%s/%d SectorSize=%d Attachment=%s Type=%s" + 410 | " ReadOnly=%t Props=%s"), 411 | d.Name, d.Path, d.Table, mbsize(d.Size), len(d.Partitions), 412 | mbsize(avail), len(fs), d.SectorSize, 413 | d.Attachment, d.Type, d.ReadOnly, d.Properties.String()) 414 | } 415 | 416 | // Details returns the disk details as a table formatted string. 417 | func (d Disk) Details() string { 418 | fss := d.FreeSpaces() 419 | var fsn int 420 | 421 | mbsize := func(n, o uint64) string { 422 | if (n+o)%Mebibyte == 0 { 423 | return fmt.Sprintf("%d MiB", (n+o)/Mebibyte) 424 | } 425 | 426 | return fmt.Sprintf("%d", n) 427 | } 428 | 429 | mbo := func(n uint64) string { return mbsize(n, 0) } 430 | mbe := func(n uint64) string { return mbsize(n, 1) } 431 | lfmt := "[%2s %10s %10s %10s %-16s %-18s ]\n" 432 | buf := fmt.Sprintf(lfmt, "#", "Start", "Last", "Size", "Name", "Type") 433 | 434 | pNums := make([]uint, 0, len(d.Partitions)) 435 | for n := range d.Partitions { 436 | pNums = append(pNums, n) 437 | } 438 | 439 | sort.Slice(pNums, func(i, j int) bool { return pNums[i] < pNums[j] }) 440 | 441 | for _, n := range pNums { 442 | p := d.Partitions[n] 443 | 444 | if fsn < len(fss) && fss[fsn].Start < p.Start { 445 | buf += fmt.Sprintf(lfmt, "-", mbo(fss[fsn].Start), mbe(fss[fsn].Last), mbo(fss[fsn].Size()), "", "None") 446 | fsn++ 447 | } 448 | 449 | name := p.Name 450 | if name == "" { 451 | name = "N/A" 452 | } 453 | 454 | buf += fmt.Sprintf(lfmt, 455 | fmt.Sprintf("%d", p.Number), mbo(p.Start), mbe(p.Last), mbo(p.Size()), name, type2str(p.Type)) 456 | } 457 | 458 | if fsn < len(fss) { 459 | buf += fmt.Sprintf(lfmt, "-", mbo(fss[fsn].Start), mbe(fss[fsn].Last), mbo(fss[fsn].Size()), "", "N/A") 460 | } 461 | 462 | return buf 463 | } 464 | 465 | func type2str(pt PartType) string { 466 | if s, ok := partid.Text[pt]; ok { 467 | return s 468 | } 469 | 470 | return pt.String() 471 | } 472 | 473 | // UdevInfo captures the udev information about a disk. 474 | type UdevInfo struct { 475 | // Name of the disk 476 | Name string `json:"name"` 477 | 478 | // SysPath is the system path of this device. 479 | SysPath string `json:"sysPath"` 480 | 481 | // Symlinks for the disk. 482 | Symlinks []string `json:"symLinks"` 483 | 484 | // Properties is udev information as a map of key, value pairs. 485 | Properties map[string]string `json:"properties"` 486 | } 487 | 488 | // PartitionSet is a map of partition number to the partition. 489 | type PartitionSet map[uint]Partition 490 | 491 | // Partition wraps the disk partition information. 492 | type Partition struct { 493 | // Start is the offset in bytes of the start of this partition. 494 | Start uint64 `json:"start"` 495 | 496 | // Last is the last byte that is part of this partition. 497 | Last uint64 `json:"last"` 498 | 499 | // ID is the partition id. 500 | ID GUID `json:"id"` 501 | 502 | // Type is the partition type. 503 | Type PartType `json:"type"` 504 | 505 | // Name is the name of this partition. 506 | Name string `json:"name"` 507 | 508 | // Number is the number of this partition. 509 | Number uint `json:"number"` 510 | } 511 | 512 | // Size returns the size of the partition in bytes. 513 | func (p *Partition) Size() uint64 { 514 | return p.Last - p.Start + 1 515 | } 516 | 517 | // jPartition - Partition, but for json (ids are strings) 518 | type jPartition struct { 519 | Start uint64 `json:"start"` 520 | Last uint64 `json:"last"` 521 | ID string `json:"id"` 522 | Type string `json:"type"` 523 | Name string `json:"name"` 524 | Number uint `json:"number"` 525 | } 526 | 527 | // UnmarshalJSON - unserialize from json 528 | func (p *Partition) UnmarshalJSON(b []byte) error { 529 | j := jPartition{} 530 | 531 | err := json.Unmarshal(b, &j) 532 | if err != nil { 533 | return err 534 | } 535 | 536 | id, err := StringToGUID(j.ID) 537 | if err != nil { 538 | return err 539 | } 540 | 541 | ptype, err := StringToGUID(j.Type) 542 | if err != nil { 543 | return err 544 | } 545 | 546 | p.Start = j.Start 547 | p.Last = j.Last 548 | p.ID = id 549 | p.Type = PartType(ptype) 550 | p.Name = j.Name 551 | p.Number = j.Number 552 | 553 | return nil 554 | } 555 | 556 | // MarshalJSON - serialize to json 557 | func (p Partition) MarshalJSON() ([]byte, error) { 558 | return json.Marshal(jPartition{ 559 | Start: p.Start, 560 | Last: p.Last, 561 | ID: p.ID.String(), 562 | Type: p.Type.String(), 563 | Name: p.Name, 564 | Number: p.Number, 565 | }) 566 | } 567 | 568 | func (p PartType) String() string { 569 | return GUIDToString(GUID(p)) 570 | } 571 | 572 | // StringToPartType - convert a string to a partition type. 573 | func StringToPartType(s string) (PartType, error) { 574 | p, err := StringToGUID(s) 575 | if err != nil { 576 | return PartType{}, err 577 | } 578 | 579 | return PartType(p), nil 580 | } 581 | 582 | // FreeSpace indicates a free slot on the disk with a Start and Last offset, 583 | // where a partition can be created. 584 | type FreeSpace struct { 585 | Start uint64 `json:"start"` 586 | Last uint64 `json:"last"` 587 | } 588 | 589 | // Size returns the size of the free space, which is End - Start. 590 | func (f *FreeSpace) Size() uint64 { 591 | return f.Last - f.Start + 1 592 | } 593 | -------------------------------------------------------------------------------- /linux/lvm.go: -------------------------------------------------------------------------------- 1 | package linux 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | "path" 9 | "strings" 10 | 11 | "machinerun.io/disko" 12 | ) 13 | 14 | const pvMetaDataSize = 128 * disko.Mebibyte 15 | const thinPoolMetaDataSize = 1024 * disko.Mebibyte 16 | 17 | // VolumeManager returns the linux implementation of disko.VolumeManager interface. 18 | func VolumeManager() disko.VolumeManager { 19 | return &linuxLVM{} 20 | } 21 | 22 | type linuxLVM struct { 23 | } 24 | 25 | func (ls *linuxLVM) ScanPVs(filter disko.PVFilter) (disko.PVSet, error) { 26 | return ls.scanPVs(filter) 27 | } 28 | 29 | func (ls *linuxLVM) scanPVs(filter disko.PVFilter, scanArgs ...string) (disko.PVSet, error) { 30 | pvs := disko.PVSet{} 31 | 32 | pvdatum, err := getPvReport(scanArgs...) 33 | if err != nil { 34 | return pvs, err 35 | } 36 | 37 | for _, pvd := range pvdatum { 38 | pv := pvd.toPV() 39 | if filter(pv) { 40 | pvs[pv.Name] = pv 41 | } 42 | } 43 | 44 | return pvs, nil 45 | } 46 | 47 | func (ls *linuxLVM) ScanVGs(filter disko.VGFilter) (disko.VGSet, error) { 48 | return ls.scanVGs(filter) 49 | } 50 | 51 | func (ls *linuxLVM) scanVGs(filter disko.VGFilter, scanArgs ...string) (disko.VGSet, error) { 52 | var vgdatum []lvmVGData 53 | var vgs = disko.VGSet{} 54 | var err error 55 | 56 | vgdatum, err = getVgReport(scanArgs...) 57 | if err != nil { 58 | return vgs, err 59 | } 60 | 61 | if len(vgdatum) == 0 { 62 | return vgs, err 63 | } 64 | 65 | for _, vgd := range vgdatum { 66 | name := vgd.Name 67 | vg := disko.VG{ 68 | Name: name, 69 | UUID: vgd.UUID, 70 | Size: vgd.Size, 71 | FreeSpace: vgd.Free, 72 | } 73 | 74 | if !filter(vg) { 75 | continue 76 | } 77 | 78 | vgs[name] = vg 79 | } 80 | 81 | if len(vgs) == 0 { 82 | return vgs, nil 83 | } 84 | 85 | fullVgs := disko.VGSet{} 86 | lvSetsByVG := map[string]disko.LVSet{} 87 | pvSetsByVG := map[string]disko.PVSet{} 88 | 89 | lvs, err := ls.scanLVs(func(d disko.LV) bool { return true }) 90 | 91 | if err != nil { 92 | return vgs, err 93 | } 94 | 95 | for _, lv := range lvs { 96 | if _, ok := lvSetsByVG[lv.VGName]; ok { 97 | lvSetsByVG[lv.VGName][lv.Name] = lv 98 | } else { 99 | lvSetsByVG[lv.VGName] = disko.LVSet{lv.Name: lv} 100 | } 101 | } 102 | 103 | pvs, err := ls.scanPVs(func(d disko.PV) bool { return true }) 104 | 105 | if err != nil { 106 | return vgs, err 107 | } 108 | 109 | for _, pv := range pvs { 110 | if _, ok := pvSetsByVG[pv.VGName]; ok { 111 | pvSetsByVG[pv.VGName][pv.Name] = pv 112 | } else { 113 | pvSetsByVG[pv.VGName] = disko.PVSet{pv.Name: pv} 114 | } 115 | } 116 | 117 | for _, vg := range vgs { 118 | vg.PVs = pvSetsByVG[vg.Name] 119 | vg.Volumes = lvSetsByVG[vg.Name] 120 | fullVgs[vg.Name] = vg 121 | } 122 | 123 | return fullVgs, nil 124 | } 125 | 126 | func (ls *linuxLVM) ScanLVs(filter disko.LVFilter) (disko.LVSet, error) { 127 | return ls.scanLVs(filter) 128 | } 129 | 130 | func (ls *linuxLVM) scanLVs(filter disko.LVFilter, scanArgs ...string) (disko.LVSet, error) { 131 | var lvdatum []lvmLVData 132 | var lvs = disko.LVSet{} 133 | var err error 134 | 135 | lvdatum, err = getLvReport(scanArgs...) 136 | if err != nil { 137 | return lvs, err 138 | } 139 | 140 | var crypt bool 141 | var cryptName, cryptPath string 142 | 143 | for _, lvd := range lvdatum { 144 | lv := lvd.toLV() 145 | 146 | if crypt, cryptName, cryptPath, err = getLuksInfo(lv.Path); err != nil { 147 | return lvs, err 148 | } 149 | 150 | lv.Encrypted = crypt 151 | if cryptName != "" { 152 | lv.DecryptedLVName = cryptName 153 | lv.DecryptedLVPath = cryptPath 154 | } 155 | 156 | if !filter(lv) { 157 | continue 158 | } 159 | 160 | lvs[lv.Name] = lv 161 | } 162 | 163 | return lvs, nil 164 | } 165 | 166 | func (ls *linuxLVM) CreatePV(name string) (disko.PV, error) { 167 | nilPV := disko.PV{} 168 | 169 | var err error 170 | var kname, path string 171 | 172 | if kname, path, err = getKnameAndPathForBlockDevice(name); err != nil { 173 | return nilPV, err 174 | } 175 | 176 | err = runCommandSettled("lvm", "pvcreate", "--force", "--zero=y", 177 | fmt.Sprintf("--metadatasize=%dB", pvMetaDataSize), path) 178 | 179 | if err != nil { 180 | return nilPV, err 181 | } 182 | 183 | pvs, err := ls.scanPVs(func(d disko.PV) bool { return true }, path) 184 | if err != nil { 185 | return nilPV, err 186 | } 187 | 188 | if len(pvs) != 1 { 189 | return nilPV, 190 | fmt.Errorf("found %d PVs named %s: %v", len(pvs), kname, pvs) 191 | } 192 | 193 | return pvs[kname], nil 194 | } 195 | 196 | func (ls *linuxLVM) DeletePV(pv disko.PV) error { 197 | return runCommandSettled("lvm", "pvremove", "--force", "--force", "--yes", pv.Path) 198 | } 199 | 200 | func (ls *linuxLVM) HasPV(name string) bool { 201 | pvs, err := ls.scanPVs(func(d disko.PV) bool { return true }, getPathForKname(name)) 202 | if err != nil { 203 | return false 204 | } 205 | 206 | return len(pvs) != 0 207 | } 208 | 209 | func (ls *linuxLVM) CreateVG(name string, pvs ...disko.PV) (disko.VG, error) { 210 | cmd := []string{"lvm", "vgcreate", "--force", "--zero=y", 211 | fmt.Sprintf("--metadatasize=%dB", pvMetaDataSize), name} 212 | 213 | for _, p := range pvs { 214 | cmd = append(cmd, p.Path) 215 | } 216 | 217 | err := runCommandSettled(cmd...) 218 | if err != nil { 219 | return disko.VG{}, err 220 | } 221 | 222 | vgSet, err := ls.scanVGs(func(d disko.VG) bool { return true }, name) 223 | 224 | if err != nil { 225 | return disko.VG{}, err 226 | } 227 | 228 | return vgSet[name], nil 229 | } 230 | 231 | func (ls *linuxLVM) ExtendVG(vgName string, pvs ...disko.PV) error { 232 | // Have to create the PVs first in case they were dirty. 233 | // pvcreate can be run on existing pvs. 234 | // https://bugzilla.redhat.com/show_bug.cgi?id=2134912 235 | pvPaths := []string{} 236 | for _, p := range pvs { 237 | pvPaths = append(pvPaths, p.Path) 238 | } 239 | 240 | cmd := append([]string{"lvm", "pvcreate", "--force", "--zero=y", 241 | fmt.Sprintf("--metadatasize=%dB", pvMetaDataSize)}, pvPaths...) 242 | 243 | err := runCommandSettled(cmd...) 244 | if err != nil { 245 | return err 246 | } 247 | 248 | cmd = append([]string{"lvm", "vgextend", "--zero=y", vgName}, pvPaths...) 249 | 250 | err = runCommandSettled(cmd...) 251 | if err != nil { 252 | return err 253 | } 254 | 255 | return nil 256 | } 257 | 258 | func (ls *linuxLVM) RemoveVG(vgName string) error { 259 | return runCommand("lvm", "vgremove", "--force", vgName) 260 | } 261 | 262 | func (ls *linuxLVM) HasVG(vgName string) bool { 263 | vgs, err := ls.scanVGs(func(d disko.VG) bool { return true }, vgName) 264 | if err != nil { 265 | return false 266 | } 267 | 268 | return len(vgs) != 0 269 | } 270 | 271 | func (ls *linuxLVM) CryptFormat(vgName string, lvName string, key string) error { 272 | return runCommandStdin( 273 | key, 274 | "cryptsetup", "luksFormat", "--type=luks2", "--key-file=-", lvPath(vgName, lvName)) 275 | } 276 | 277 | func (ls *linuxLVM) CryptOpen(vgName string, lvName string, 278 | decryptedName string, key string) error { 279 | return runCommandStdin(key, 280 | "cryptsetup", "open", "--type=luks", "--key-file=-", 281 | lvPath(vgName, lvName), decryptedName) 282 | } 283 | 284 | func (ls *linuxLVM) CryptClose(vgName string, lvName string, 285 | decryptedName string) error { 286 | return runCommand("cryptsetup", "close", decryptedName) 287 | } 288 | 289 | func createLVCmd(args ...string) error { 290 | return runCommandSettled( 291 | append([]string{"lvm", "lvcreate", "--ignoremonitoring", "--yes", "--activate=y", 292 | "--setactivationskip=n"}, args...)...) 293 | } 294 | 295 | func createThinPool(name string, vgName string, size uint64, mdSize uint64) error { 296 | // thinpool takes up size + 2*mdSize 297 | // https://www.redhat.com/archives/linux-lvm/2020-October/thread.html#00016 298 | args := []string{} 299 | // if mdSize is zero, let lvcreate choose the size. That is documented as: 300 | // (Pool_LV_size / Pool_LV_chunk_size * 64) 301 | if mdSize != 0 { 302 | args = append(args, fmt.Sprintf("--poolmetadatasize=%dB", mdSize)) 303 | } 304 | 305 | return createLVCmd(append(args, "--zero=y", "--wipesignatures=y", 306 | fmt.Sprintf("--size=%dB", size), "--thinpool="+name, vgName)...) 307 | } 308 | 309 | func (ls *linuxLVM) CreateLV(vgName string, name string, size uint64, 310 | lvType disko.LVType) (disko.LV, error) { 311 | nilLV := disko.LV{} 312 | 313 | if err := isRoundExtent(size); err != nil { 314 | return nilLV, err 315 | } 316 | 317 | nameFlag := "--name=" + name 318 | sizeB := fmt.Sprintf("%dB", size) 319 | vglv := vgLv(vgName, name) 320 | 321 | // Missing cases: LVTypeUnknown 322 | //exhaustive:ignore 323 | switch lvType { 324 | case disko.THIN: 325 | // When creating THIN LV, the VG must be / 326 | if !strings.Contains(vgName, "/") { 327 | return nilLV, 328 | fmt.Errorf("%s: vgName input for THIN LV name in format /thinDataName", vgName) 329 | } 330 | 331 | vglv = vgLv(strings.Split(vgName, "/")[0], name) 332 | 333 | // creation of thin volumes are always zero'd, and passing '--zero=y' will fail. 334 | if err := createLVCmd("--virtualsize="+sizeB, nameFlag, vgName); err != nil { 335 | return nilLV, err 336 | } 337 | case disko.THICK: 338 | if err := createLVCmd("--zero=y", "--wipesignatures=y", "--size="+sizeB, nameFlag, vgName); err != nil { 339 | return nilLV, err 340 | } 341 | 342 | if err := luks2Wipe(lvPath(vgName, name)); err != nil { 343 | return nilLV, err 344 | } 345 | case disko.THINPOOL: 346 | // When creating a THINPOOL, the name is the thin pool name. 347 | if err := createThinPool(name, vgName, size, thinPoolMetaDataSize); err != nil { 348 | return nilLV, err 349 | } 350 | } 351 | 352 | lvs, err := ls.scanLVs(func(d disko.LV) bool { return true }, vglv) 353 | 354 | if err != nil { 355 | return nilLV, err 356 | } 357 | 358 | if len(lvs) != 1 { 359 | return nilLV, fmt.Errorf("found %d LVs with %s/%s", len(lvs), vgName, name) 360 | } 361 | 362 | return lvs[name], nil 363 | } 364 | 365 | // luks2Wipe - wipe luks2 from a file/device. 366 | // libblkid (used by wipefs and lvm) did not gain full wiping of luks2 metadata until 2.33. 367 | // Wipe it more completely here. 368 | func luks2Wipe(fpath string) error { 369 | const zeroLen = 64 370 | bufZero := make([]byte, zeroLen) 371 | 372 | // possible offsets for luks2 seconday headers from cryptsetup/lib/luks2/luks2.h 373 | offsets := []int64{ 374 | 0x04000, 0x008000, 0x010000, 0x020000, 0x40000, 375 | 0x080000, 0x100000, 0x200000, 0x400000} 376 | 377 | return withLockedFile(fpath, 378 | func(fp *os.File, fInfo os.FileInfo) error { 379 | var wlen int64 380 | fileLen, err := fp.Seek(0, io.SeekEnd) 381 | if err != nil { 382 | return err 383 | } 384 | for _, offset := range offsets { 385 | wlen = zeroLen 386 | if offset >= fileLen { 387 | continue 388 | } else if offset > (fileLen - zeroLen) { 389 | wlen = fileLen - offset 390 | } 391 | if _, err := fp.Seek(offset, io.SeekStart); err != nil { 392 | return err 393 | } 394 | if n, err := fp.Write(bufZero[:wlen]); err != nil { 395 | return err 396 | } else if n != int(wlen) { 397 | return fmt.Errorf("short write on %s at offset %x. wrote %d, tried %d", 398 | fpath, offset, n, zeroLen) 399 | } 400 | } 401 | return nil 402 | }) 403 | } 404 | 405 | func (ls *linuxLVM) RenameLV(vgName string, lvName string, newLvName string) error { 406 | return runCommandSettled("lvm", "lvrename", vgName, lvName, newLvName) 407 | } 408 | 409 | func (ls *linuxLVM) RemoveLV(vgName string, lvName string) error { 410 | return runCommandSettled( 411 | "lvm", "lvremove", "--force", "--force", vgLv(vgName, lvName)) 412 | } 413 | 414 | func (ls *linuxLVM) ExtendLV(vgName string, lvName string, 415 | newSize uint64) error { 416 | var err error 417 | 418 | if err = isRoundExtent(newSize); err != nil { 419 | return err 420 | } 421 | 422 | err = runCommandSettled( 423 | "lvm", "lvextend", fmt.Sprintf("--size=%dB", newSize), 424 | vgLv(vgName, lvName)) 425 | 426 | if err != nil { 427 | return err 428 | } 429 | 430 | if crypt, cryptName, _, err := getLuksInfo(lvPath(vgName, lvName)); err != nil { 431 | return err 432 | } else if crypt && cryptName != "" { 433 | // luks device already opened, so resize it. 434 | if err := runCommandSettled("cryptsetup", "resize", cryptName); err != nil { 435 | return err 436 | } 437 | } 438 | 439 | return nil 440 | } 441 | 442 | func (ls *linuxLVM) HasLV(vgName string, name string) bool { 443 | lvs, err := ls.scanLVs(func(d disko.LV) bool { return true }, vgLv(vgName, name)) 444 | if err != nil { 445 | log.Panicf("Failed to scan logical volumes: %s", err) 446 | } 447 | 448 | return len(lvs) != 0 449 | } 450 | 451 | func isRoundExtent(size uint64) error { 452 | if size%disko.ExtentSize == 0 { 453 | return nil 454 | } 455 | 456 | return fmt.Errorf("%d is not evenly divisible by extent size %d", 457 | size, disko.ExtentSize) 458 | } 459 | 460 | // chompBytes - strip one trailing newline if present. 461 | func chompBytes(data []byte) []byte { 462 | l := len(data) 463 | if l == 0 || data[l-1] != '\n' { 464 | return data 465 | } 466 | 467 | return data[:l-1] 468 | } 469 | 470 | // getLuksInfo - get luks information for the provided block device path) 471 | // returns: 472 | // 473 | // crypt - boolean indicating if device is encrypted. 474 | // cryptName - name of crypt dev if device is open - "" if not encrypted. 475 | // cryptPath - path of crypt dev if device is open - "" if not encrypted. 476 | // error - nil unless an error occurred. 477 | func getLuksInfo(devpath string) (bool, string, string, error) { 478 | crypt := false 479 | 480 | if !pathExists(devpath) { 481 | return crypt, "", "", nil 482 | } 483 | 484 | // $ cryptsetup luksUUID /dev/vg_ifc0/certs 485 | // a41a29c5-e375-4586-b30f-40eee4441db6 486 | cmd := []string{"cryptsetup", "luksUUID", devpath} 487 | stdout, stderr, rc := runCommandWithOutputErrorRc(cmd...) 488 | 489 | if rc == 1 { 490 | return crypt, "", "", nil 491 | } else if rc != 0 { 492 | return crypt, "", "", cmdError(cmd, stdout, stderr, rc) 493 | } 494 | // prefix looks like CRYPT-LUKS[12]-- 495 | bareID := strings.ReplaceAll(string(chompBytes(stdout)), "-", "") 496 | luks1 := "CRYPT-LUKS1-" + bareID + "-" 497 | luks2 := "CRYPT-LUKS2-" + bareID + "-" 498 | 499 | crypt = true 500 | minFields := 4 501 | 502 | cmd = []string{"dmsetup", "table", "--concise"} 503 | stdout, stderr, rc = runCommandWithOutputErrorRc(cmd...) 504 | 505 | if rc != 0 { 506 | return crypt, "", "", cmdError(cmd, stdout, stderr, rc) 507 | } 508 | 509 | // dmsetup table --concise returns semi-colon delimited records that are comma separated. 510 | // per dmsetup(8): The representation of a device takes the form: 511 | // ,,,,[,
+] 512 | for _, record := range strings.Split(string(chompBytes(stdout)), ";") { 513 | fields := strings.Split(record, ",") 514 | if len(fields) < minFields { 515 | return crypt, "", "", 516 | fmt.Errorf( 517 | "unexpected data in dmsetup table --concise. Found %d fields, expected >= %d: %s", 518 | len(fields), minFields, record) 519 | } 520 | 521 | if strings.HasPrefix(fields[1], luks1) || strings.HasPrefix(fields[1], luks2) { 522 | return crypt, fields[0], "/dev/mapper/" + fields[0], nil 523 | } 524 | } 525 | 526 | return crypt, "", "", nil 527 | } 528 | 529 | func (d *lvmLVData) toLV() disko.LV { 530 | lvtype := disko.THICK 531 | 532 | var isThin, isPool = false, false 533 | 534 | for _, l := range strings.Split(d.raw["lv_layout"], ",") { 535 | if l == "thin" { 536 | isThin = true 537 | } 538 | 539 | if l == "pool" { 540 | isPool = true 541 | } 542 | } 543 | 544 | if isPool { 545 | lvtype = disko.THINPOOL 546 | } else if isThin { 547 | lvtype = disko.THIN 548 | } 549 | 550 | lv := disko.LV{ 551 | Name: d.Name, 552 | UUID: d.UUID, 553 | Path: d.Path, 554 | VGName: d.VGName, 555 | Size: d.Size, 556 | Type: lvtype, 557 | Encrypted: false, 558 | } 559 | 560 | return lv 561 | } 562 | 563 | func (d *lvmPVData) toPV() disko.PV { 564 | return disko.PV{ 565 | Path: d.Path, 566 | UUID: d.UUID, 567 | Name: path.Base(d.Path), 568 | Size: d.Size, 569 | VGName: d.VGName, 570 | FreeSize: d.Free, 571 | } 572 | } 573 | --------------------------------------------------------------------------------