From 2e1946a200bf2d6374ecc097916d664f49fd8417 Mon Sep 17 00:00:00 2001 From: Luke Shumaker Date: Sun, 5 Mar 2023 21:54:14 -0700 Subject: Factor out the device-scan loop from rebuildmappings in to btrfsutil --- lib/btrfsutil/scan.go | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 lib/btrfsutil/scan.go (limited to 'lib') diff --git a/lib/btrfsutil/scan.go b/lib/btrfsutil/scan.go new file mode 100644 index 0000000..97220aa --- /dev/null +++ b/lib/btrfsutil/scan.go @@ -0,0 +1,148 @@ +// Copyright (C) 2022-2023 Luke Shumaker +// +// SPDX-License-Identifier: GPL-2.0-or-later + +package btrfsutil + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/datawire/dlib/dgroup" + "github.com/datawire/dlib/dlog" + + "git.lukeshu.com/btrfs-progs-ng/lib/binstruct" + "git.lukeshu.com/btrfs-progs-ng/lib/btrfs" + "git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfssum" + "git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfstree" + "git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfsvol" + "git.lukeshu.com/btrfs-progs-ng/lib/diskio" + "git.lukeshu.com/btrfs-progs-ng/lib/textui" +) + +var sbSize = btrfsvol.PhysicalAddr(binstruct.StaticSize(btrfstree.Superblock{})) + +type DeviceScannerFactory[Stats comparable, Result any] func(ctx context.Context, sb btrfstree.Superblock, numBytes btrfsvol.PhysicalAddr, numSectors int) DeviceScanner[Stats, Result] + +type DeviceScanner[Stats comparable, Result any] interface { + ScanStats() Stats + ScanSector(ctx context.Context, dev *btrfs.Device, paddr btrfsvol.PhysicalAddr) error + ScanNode(ctx context.Context, nodeRef *diskio.Ref[btrfsvol.PhysicalAddr, btrfstree.Node]) error + ScanDone(ctx context.Context) (Result, error) +} + +type scanStats[T comparable] struct { + portion textui.Portion[btrfsvol.PhysicalAddr] + stats T +} + +func (s scanStats[T]) String() string { + return textui.Sprintf("scanned %v (%v)", + s.portion, s.stats) +} + +func ScanDevices[Stats comparable, Result any](ctx context.Context, fs *btrfs.FS, newScanner DeviceScannerFactory[Stats, Result]) (map[btrfsvol.DeviceID]Result, error) { + grp := dgroup.NewGroup(ctx, dgroup.GroupConfig{}) + var mu sync.Mutex + result := make(map[btrfsvol.DeviceID]Result) + for id, dev := range fs.LV.PhysicalVolumes() { + id := id + dev := dev + grp.Go(fmt.Sprintf("dev-%d", id), func(ctx context.Context) error { + devResult, err := ScanOneDevice[Stats, Result](ctx, dev, newScanner) + if err != nil { + return err + } + mu.Lock() + result[id] = devResult + mu.Unlock() + return nil + }) + } + if err := grp.Wait(); err != nil { + return nil, err + } + return result, nil +} + +func ScanOneDevice[Stats comparable, Result any](ctx context.Context, dev *btrfs.Device, newScanner DeviceScannerFactory[Stats, Result]) (Result, error) { + ctx = dlog.WithField(ctx, "scandevices.dev", dev.Name()) + + sb, err := dev.Superblock() + if err != nil { + var zero Result + return zero, err + } + numBytes := dev.Size() + if sb.NodeSize < sb.SectorSize { + var zero Result + return zero, fmt.Errorf("node_size(%v) < sector_size(%v)", + sb.NodeSize, sb.SectorSize) + } + if sb.SectorSize != btrfssum.BlockSize { + // TODO: probably handle this? + var zero Result + return zero, fmt.Errorf("sector_size(%v) != btrfssum.BlockSize", + sb.SectorSize) + } + numSectors := int(numBytes / btrfssum.BlockSize) + + scanner := newScanner(ctx, *sb, numBytes, numSectors) + + progressWriter := textui.NewProgress[scanStats[Stats]](ctx, dlog.LogLevelInfo, textui.Tunable(1*time.Second)) + var stats scanStats[Stats] + stats.portion.D = numBytes + + var minNextNode btrfsvol.PhysicalAddr + for i := 0; i < numSectors; i++ { + if ctx.Err() != nil { + var zero Result + return zero, ctx.Err() + } + pos := btrfsvol.PhysicalAddr(i * btrfssum.BlockSize) + stats.portion.N = pos + stats.stats = scanner.ScanStats() + progressWriter.Set(stats) + + if err := scanner.ScanSector(ctx, dev, pos); err != nil { + var zero Result + return zero, err + } + + checkForNode := pos >= minNextNode && pos+btrfsvol.PhysicalAddr(sb.NodeSize) <= numBytes + if checkForNode { + for _, sbAddr := range btrfs.SuperblockAddrs { + if sbAddr <= pos && pos < sbAddr+sbSize { + checkForNode = false + break + } + } + } + + if checkForNode { + nodeRef, err := btrfstree.ReadNode[btrfsvol.PhysicalAddr](dev, *sb, pos, btrfstree.NodeExpectations{}) + if err != nil { + if !errors.Is(err, btrfstree.ErrNotANode) { + dlog.Errorf(ctx, "error: %v", err) + } + } else { + if err := scanner.ScanNode(ctx, nodeRef); err != nil { + var zero Result + return zero, err + } + minNextNode = pos + btrfsvol.PhysicalAddr(sb.NodeSize) + } + btrfstree.FreeNodeRef(nodeRef) + } + } + + stats.portion.N = numBytes + stats.stats = scanner.ScanStats() + progressWriter.Set(stats) + progressWriter.Done() + + return scanner.ScanDone(ctx) +} -- cgit v1.2.3-54-g00ecf From 3949bd3ff240bc6d06dd08f6e3183e72571e0e1d Mon Sep 17 00:00:00 2001 From: Luke Shumaker Date: Tue, 7 Mar 2023 12:10:42 -0700 Subject: Expose node-lists as a thing on the CLI --- cmd/btrfs-rec/inspect_listnodes.go | 51 ++++++++++++++++++++++++ cmd/btrfs-rec/inspect_lstrees.go | 23 ++++++++--- cmd/btrfs-rec/inspect_rebuildmappings.go | 44 +++++++++++++++++++++ cmd/btrfs-rec/inspect_rebuildtrees.go | 46 +++++++++++++++------- cmd/btrfs-rec/util.go | 25 ------------ lib/btrfsutil/listnodes.go | 66 ++++++++++++++++++++++++++++++++ scripts/main.sh | 6 ++- 7 files changed, 214 insertions(+), 47 deletions(-) create mode 100644 cmd/btrfs-rec/inspect_listnodes.go create mode 100644 lib/btrfsutil/listnodes.go (limited to 'lib') diff --git a/cmd/btrfs-rec/inspect_listnodes.go b/cmd/btrfs-rec/inspect_listnodes.go new file mode 100644 index 0000000..d9b24ed --- /dev/null +++ b/cmd/btrfs-rec/inspect_listnodes.go @@ -0,0 +1,51 @@ +// Copyright (C) 2022-2023 Luke Shumaker +// +// SPDX-License-Identifier: GPL-2.0-or-later + +package main + +import ( + "os" + + "git.lukeshu.com/go/lowmemjson" + "github.com/datawire/dlib/dlog" + "github.com/datawire/ocibuild/pkg/cliutil" + "github.com/spf13/cobra" + + "git.lukeshu.com/btrfs-progs-ng/lib/btrfs" + "git.lukeshu.com/btrfs-progs-ng/lib/btrfsutil" +) + +func init() { + inspectors.AddCommand(&cobra.Command{ + Use: "list-nodes", + Short: "Scan the filesystem for btree nodes", + Long: "" + + "This scans the filesystem sector-by-sector looking for nodes. " + + "If you are needing to rebuild the chunk/dev-extent/blockgroup " + + "trees with `btrfs-rec inspect rebuild-mappings` anyway, you may " + + "want to instead use `btrfs-rec inspect rebuild-mappings list-nodes` " + + "to take advantage of the sector-by-sector scan that's already " + + "performed by `btrfs-rec inspect rebuild-mappings scan`.", + Args: cliutil.WrapPositionalArgs(cobra.ExactArgs(1)), + RunE: runWithRawFS(func(fs *btrfs.FS, cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + nodeList, err := btrfsutil.ListNodes(ctx, fs) + if err != nil { + return err + } + + dlog.Infof(ctx, "Writing nodes to stdout...") + if err := writeJSONFile(os.Stdout, nodeList, lowmemjson.ReEncoderConfig{ + Indent: "\t", + ForceTrailingNewlines: true, + }); err != nil { + return err + } + dlog.Info(ctx, "... done writing") + + return nil + }), + }) +} diff --git a/cmd/btrfs-rec/inspect_lstrees.go b/cmd/btrfs-rec/inspect_lstrees.go index 1ff7671..05c3a57 100644 --- a/cmd/btrfs-rec/inspect_lstrees.go +++ b/cmd/btrfs-rec/inspect_lstrees.go @@ -26,14 +26,24 @@ import ( ) func init() { - var scandevicesFilename string + var nodeListFilename string cmd := &cobra.Command{ Use: "ls-trees", Short: "A brief view what types of items are in each tree", - Args: cliutil.WrapPositionalArgs(cobra.NoArgs), + Long: "" + + "If no --node-list is given, then a slow sector-by-sector scan " + + "will be used to find all lost+found nodes.", + Args: cliutil.WrapPositionalArgs(cobra.NoArgs), RunE: runWithRawFS(func(fs *btrfs.FS, cmd *cobra.Command, _ []string) error { ctx := cmd.Context() - nodeList, err := readNodeList(ctx, scandevicesFilename) + + var nodeList []btrfsvol.LogicalAddr + var err error + if nodeListFilename != "" { + nodeList, err = readJSONFile[[]btrfsvol.LogicalAddr](ctx, nodeListFilename) + } else { + nodeList, err = btrfsutil.ListNodes(ctx, fs) + } if err != nil { return err } @@ -86,7 +96,7 @@ func init() { }, }) - if scandevicesFilename != "" { + { treeErrCnt = 0 treeItemCnt = make(map[btrfsitem.Type]int) textui.Fprintf(os.Stdout, "lost+found\n") @@ -114,8 +124,9 @@ func init() { return nil }), } - cmd.Flags().StringVar(&scandevicesFilename, "scandevices", "", "Output of 'btrfs-recs inspect rebuild-mappings scan' to use for a lost+found tree") - noError(cmd.MarkFlagFilename("scandevices")) + cmd.Flags().StringVar(&nodeListFilename, "node-list", "", + "Output of 'btrfs-recs inspect [rebuild-mappings] list-nodes' to use for a lost+found tree") + noError(cmd.MarkFlagFilename("node-list")) inspectors.AddCommand(cmd) } diff --git a/cmd/btrfs-rec/inspect_rebuildmappings.go b/cmd/btrfs-rec/inspect_rebuildmappings.go index 9c6b1cc..b215e7a 100644 --- a/cmd/btrfs-rec/inspect_rebuildmappings.go +++ b/cmd/btrfs-rec/inspect_rebuildmappings.go @@ -14,6 +14,9 @@ import ( "git.lukeshu.com/btrfs-progs-ng/cmd/btrfs-rec/inspect/rebuildmappings" "git.lukeshu.com/btrfs-progs-ng/lib/btrfs" + "git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfsvol" + "git.lukeshu.com/btrfs-progs-ng/lib/containers" + "git.lukeshu.com/btrfs-progs-ng/lib/maps" ) func init() { @@ -118,5 +121,46 @@ func init() { }), }) + cmd.AddCommand(&cobra.Command{ + Use: "list-nodes", + Short: "Produce a listing of btree nodes from previously read data", + Long: "" + + "This is a variant of `btrfs-rec inspect list-nodes` that takes " + + "advantage of using previously read data from " + + "`btrfs-rec inspect rebuild-nodes scan`.", + Args: cliutil.WrapPositionalArgs(cobra.ExactArgs(1)), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + scanResults, err := readJSONFile[rebuildmappings.ScanDevicesResult](ctx, args[0]) + if err != nil { + return err + } + + var cnt int + for _, devResults := range scanResults { + cnt += len(devResults.FoundNodes) + } + set := make(containers.Set[btrfsvol.LogicalAddr], cnt) + for _, devResults := range scanResults { + for laddr := range devResults.FoundNodes { + set.Insert(laddr) + } + } + nodeList := maps.SortedKeys(set) + + dlog.Infof(ctx, "Writing nodes to stdout...") + if err := writeJSONFile(os.Stdout, nodeList, lowmemjson.ReEncoderConfig{ + Indent: "\t", + ForceTrailingNewlines: true, + }); err != nil { + return err + } + dlog.Info(ctx, "... done writing") + + return nil + }, + }) + inspectors.AddCommand(cmd) } diff --git a/cmd/btrfs-rec/inspect_rebuildtrees.go b/cmd/btrfs-rec/inspect_rebuildtrees.go index f66f287..676533a 100644 --- a/cmd/btrfs-rec/inspect_rebuildtrees.go +++ b/cmd/btrfs-rec/inspect_rebuildtrees.go @@ -5,7 +5,6 @@ package main import ( - "context" "os" "runtime" "time" @@ -17,25 +16,39 @@ import ( "git.lukeshu.com/btrfs-progs-ng/cmd/btrfs-rec/inspect/rebuildtrees" "git.lukeshu.com/btrfs-progs-ng/lib/btrfs" + "git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfsvol" + "git.lukeshu.com/btrfs-progs-ng/lib/btrfsutil" "git.lukeshu.com/btrfs-progs-ng/lib/textui" ) func init() { - inspectors.AddCommand(&cobra.Command{ - Use: "rebuild-trees NODESCAN.json", - Args: cliutil.WrapPositionalArgs(cobra.ExactArgs(1)), + var nodeListFilename string + cmd := &cobra.Command{ + Use: "rebuild-trees", + Long: "" + + "Rebuild broken btrees based on missing items that are implied " + + "by present items. This requires functioning " + + "chunk/dev-extent/blockgroup trees, which can be rebuilt " + + "separately with `btrfs-rec inspect rebuild-mappings`.\n" + + "\n" + + "If no --node-list is given, then a slow sector-by-sector scan " + + "will be used to find all nodes.", + Args: cliutil.WrapPositionalArgs(cobra.NoArgs), RunE: runWithRawFS(func(fs *btrfs.FS, cmd *cobra.Command, args []string) error { ctx := cmd.Context() - // This is wrapped in a func in order to *ensure* that `nodeList` goes out of scope once - // `rebuilder` has been created. - rebuilder, err := func(ctx context.Context) (rebuildtrees.Rebuilder, error) { - nodeList, err := readNodeList(ctx, args[0]) - if err != nil { - return nil, err - } - return rebuildtrees.NewRebuilder(ctx, fs, nodeList) - }(ctx) + var nodeList []btrfsvol.LogicalAddr + var err error + if nodeListFilename != "" { + nodeList, err = readJSONFile[[]btrfsvol.LogicalAddr](ctx, nodeListFilename) + } else { + nodeList, err = btrfsutil.ListNodes(ctx, fs) + } + if err != nil { + return err + } + + rebuilder, err := rebuildtrees.NewRebuilder(ctx, fs, nodeList) if err != nil { return err } @@ -65,5 +78,10 @@ func init() { return rebuildErr }), - }) + } + cmd.Flags().StringVar(&nodeListFilename, "node-list", "", + "Output of 'btrfs-recs inspect [rebuild-mappings] list-nodes' to use for the node list") + noError(cmd.MarkFlagFilename("node-list")) + + inspectors.AddCommand(cmd) } diff --git a/cmd/btrfs-rec/util.go b/cmd/btrfs-rec/util.go index 2e4bb84..3d751a6 100644 --- a/cmd/btrfs-rec/util.go +++ b/cmd/btrfs-rec/util.go @@ -13,10 +13,6 @@ import ( "git.lukeshu.com/go/lowmemjson" "github.com/datawire/dlib/dlog" - "git.lukeshu.com/btrfs-progs-ng/cmd/btrfs-rec/inspect/rebuildmappings" - "git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfsvol" - "git.lukeshu.com/btrfs-progs-ng/lib/containers" - "git.lukeshu.com/btrfs-progs-ng/lib/maps" "git.lukeshu.com/btrfs-progs-ng/lib/streamio" ) @@ -51,24 +47,3 @@ func writeJSONFile(w io.Writer, obj any, cfg lowmemjson.ReEncoderConfig) (err er }() return lowmemjson.NewEncoder(lowmemjson.NewReEncoder(buffer, cfg)).Encode(obj) } - -func readNodeList(ctx context.Context, filename string) ([]btrfsvol.LogicalAddr, error) { - if filename == "" { - return nil, nil - } - results, err := readJSONFile[rebuildmappings.ScanDevicesResult](ctx, filename) - if err != nil { - return nil, err - } - var cnt int - for _, devResults := range results { - cnt += len(devResults.FoundNodes) - } - set := make(containers.Set[btrfsvol.LogicalAddr], cnt) - for _, devResults := range results { - for laddr := range devResults.FoundNodes { - set.Insert(laddr) - } - } - return maps.SortedKeys(set), nil -} diff --git a/lib/btrfsutil/listnodes.go b/lib/btrfsutil/listnodes.go new file mode 100644 index 0000000..16300da --- /dev/null +++ b/lib/btrfsutil/listnodes.go @@ -0,0 +1,66 @@ +// Copyright (C) 2023 Luke Shumaker +// +// SPDX-License-Identifier: GPL-2.0-or-later + +package btrfsutil + +import ( + "context" + + "git.lukeshu.com/btrfs-progs-ng/lib/btrfs" + "git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfstree" + "git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfsvol" + "git.lukeshu.com/btrfs-progs-ng/lib/containers" + "git.lukeshu.com/btrfs-progs-ng/lib/diskio" + "git.lukeshu.com/btrfs-progs-ng/lib/maps" + "git.lukeshu.com/btrfs-progs-ng/lib/textui" +) + +type nodeScanner struct { + nodes containers.Set[btrfsvol.LogicalAddr] +} + +type nodeStats struct { + numNodes int +} + +func (s nodeStats) String() string { + return textui.Sprintf("found: %d nodes", s.numNodes) +} + +var _ DeviceScanner[nodeStats, containers.Set[btrfsvol.LogicalAddr]] = (*nodeScanner)(nil) + +func newNodeScanner(ctx context.Context, sb btrfstree.Superblock, numBytes btrfsvol.PhysicalAddr, numSectors int) DeviceScanner[nodeStats, containers.Set[btrfsvol.LogicalAddr]] { + s := new(nodeScanner) + s.nodes = make(containers.Set[btrfsvol.LogicalAddr]) + return s +} + +func (s *nodeScanner) ScanStats() nodeStats { + return nodeStats{numNodes: len(s.nodes)} +} + +func (*nodeScanner) ScanSector(ctx context.Context, dev *btrfs.Device, paddr btrfsvol.PhysicalAddr) error { + return nil +} + +func (s *nodeScanner) ScanNode(ctx context.Context, nodeRef *diskio.Ref[btrfsvol.PhysicalAddr, btrfstree.Node]) error { + s.nodes.Insert(nodeRef.Data.Head.Addr) + return nil +} + +func (s *nodeScanner) ScanDone(ctx context.Context) (containers.Set[btrfsvol.LogicalAddr], error) { + return s.nodes, nil +} + +func ListNodes(ctx context.Context, fs *btrfs.FS) ([]btrfsvol.LogicalAddr, error) { + perDev, err := ScanDevices[nodeStats, containers.Set[btrfsvol.LogicalAddr]](ctx, fs, newNodeScanner) + if err != nil { + return nil, err + } + set := make(containers.Set[btrfsvol.LogicalAddr]) + for _, devSet := range perDev { + set.InsertFrom(devSet) + } + return maps.SortedKeys(set), nil +} diff --git a/scripts/main.sh b/scripts/main.sh index 2ea5213..dddb9bb 100755 --- a/scripts/main.sh +++ b/scripts/main.sh @@ -33,6 +33,8 @@ export GOMEMLIMIT="$(awk '/^MemTotal:/{ print $2 "KiB" }'