|
| 1 | +/* |
| 2 | +Copyright 2026 Cozystack contributors. |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +package satellite |
| 18 | + |
| 19 | +import ( |
| 20 | + "context" |
| 21 | + "fmt" |
| 22 | + "strings" |
| 23 | + |
| 24 | + "github.com/cockroachdb/errors" |
| 25 | + |
| 26 | + satellitepb "github.com/cozystack/blockstor/pkg/satellite/proto" |
| 27 | + "github.com/cozystack/blockstor/pkg/storage" |
| 28 | +) |
| 29 | + |
| 30 | +// shipExec is the storage.Exec the Reconciler uses when actually |
| 31 | +// shipping. We expose it as a separate field on the Reconciler so the |
| 32 | +// snapshot-ship path can use a different exec from the storage CRUD |
| 33 | +// path (e.g. for nested ssh exec). Today we always reuse the storage |
| 34 | +// providers' exec, but it pays for itself in tests. |
| 35 | + |
| 36 | +// ShipSnapshot copies a snapshot from this satellite to a peer. The |
| 37 | +// mechanism is picked by the source pool's Kind: |
| 38 | +// - ZFS / ZFS_THIN → `zfs send <snap> | ssh peer zfs recv <dataset>` |
| 39 | +// - LVM_THIN → `thin-send-recv` (Linbit's tool) |
| 40 | +// - everything else → not supported |
| 41 | +// |
| 42 | +// The actual data flow happens out-of-process (a single piped shell |
| 43 | +// command); we record the chosen mechanism in the response so the |
| 44 | +// controller can plumb stats back to the caller. |
| 45 | +func (r *Reconciler) ShipSnapshot(ctx context.Context, req *satellitepb.ShipSnapshotRequest) (*satellitepb.ShipSnapshotResponse, error) { |
| 46 | + provider, err := r.providerForResource(req.GetResourceName()) |
| 47 | + if err != nil { |
| 48 | + //nolint:nilerr // surfaced as ok=false; gRPC error is for transport faults |
| 49 | + return &satellitepb.ShipSnapshotResponse{Ok: false, Message: err.Error()}, nil |
| 50 | + } |
| 51 | + |
| 52 | + mechanism := req.GetMechanism() |
| 53 | + if mechanism == "" { |
| 54 | + mechanism = pickMechanism(provider.Kind()) |
| 55 | + } |
| 56 | + |
| 57 | + err = r.runShip(ctx, mechanism, req) |
| 58 | + if err != nil { |
| 59 | + //nolint:nilerr // per-RPC error path |
| 60 | + return &satellitepb.ShipSnapshotResponse{Ok: false, Message: err.Error()}, nil |
| 61 | + } |
| 62 | + |
| 63 | + return &satellitepb.ShipSnapshotResponse{Ok: true}, nil |
| 64 | +} |
| 65 | + |
| 66 | +// runShip dispatches to the right shell pipeline given the chosen |
| 67 | +// mechanism. Errors from the exec layer surface to the caller. |
| 68 | +func (r *Reconciler) runShip(ctx context.Context, mechanism string, req *satellitepb.ShipSnapshotRequest) error { |
| 69 | + exec := r.shipExec() |
| 70 | + |
| 71 | + switch strings.ToLower(mechanism) { |
| 72 | + case mechanismZFS, "zfs-send": |
| 73 | + return runZfsShip(ctx, exec, req) |
| 74 | + case mechanismThin, "lvm-thin", "thin-send-recv": |
| 75 | + return runThinShip(ctx, exec, req) |
| 76 | + default: |
| 77 | + return errors.Errorf("unsupported snapshot-ship mechanism %q", mechanism) |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +// shipExec returns the storage.Exec the ship pipeline runs under. In |
| 82 | +// production this is `storage.RealExec`; tests inject a `FakeExec` via |
| 83 | +// `ReconcilerConfig.ShipExec` so they can assert command lines |
| 84 | +// without spinning up the real zfs / ssh / thin-send-recv tools. |
| 85 | +func (r *Reconciler) shipExec() storage.Exec { |
| 86 | + if r.cfg.ShipExec != nil { |
| 87 | + return r.cfg.ShipExec |
| 88 | + } |
| 89 | + |
| 90 | + return storage.RealExec{} |
| 91 | +} |
| 92 | + |
| 93 | +// runZfsShip pipes `zfs send` into `ssh <target> zfs recv`. We don't |
| 94 | +// shell out via /bin/sh -c here — the storage.Exec abstraction runs |
| 95 | +// one binary, so we wrap the pipe ourselves through `sh -c`. |
| 96 | +func runZfsShip(ctx context.Context, exec storage.Exec, req *satellitepb.ShipSnapshotRequest) error { |
| 97 | + pipeline := fmt.Sprintf("zfs send %s | ssh %s zfs recv -F %s", |
| 98 | + req.GetSnapshotName(), req.GetTargetNode(), req.GetResourceName()) |
| 99 | + |
| 100 | + _, err := exec.Run(ctx, "sh", "-c", pipeline) |
| 101 | + if err != nil { |
| 102 | + return errors.Wrap(err, "zfs send|recv") |
| 103 | + } |
| 104 | + |
| 105 | + return nil |
| 106 | +} |
| 107 | + |
| 108 | +// runThinShip uses Linbit's `thin-send-recv` helper, which understands |
| 109 | +// the LVM-thin metadata block format and drives a similar pipe over |
| 110 | +// ssh under the hood. |
| 111 | +func runThinShip(ctx context.Context, exec storage.Exec, req *satellitepb.ShipSnapshotRequest) error { |
| 112 | + _, err := exec.Run(ctx, "thin-send-recv", |
| 113 | + "--source", req.GetResourceName()+"_"+req.GetSnapshotName()+"_00000", |
| 114 | + "--target", req.GetTargetNode()) |
| 115 | + if err != nil { |
| 116 | + return errors.Wrap(err, "thin-send-recv") |
| 117 | + } |
| 118 | + |
| 119 | + return nil |
| 120 | +} |
| 121 | + |
| 122 | +// pickMechanism maps a provider Kind onto the default shipping |
| 123 | +// mechanism string. Kinds we don't know about return "" — the caller |
| 124 | +// surfaces that as an unsupported-mechanism error. |
| 125 | +func pickMechanism(kind string) string { |
| 126 | + switch kind { |
| 127 | + case kindZFS, kindZFSThin: |
| 128 | + return mechanismZFS |
| 129 | + case kindLVMThin: |
| 130 | + return mechanismThin |
| 131 | + default: |
| 132 | + return "" |
| 133 | + } |
| 134 | +} |
| 135 | + |
| 136 | +const ( |
| 137 | + mechanismZFS = "zfs" |
| 138 | + mechanismThin = "thin" |
| 139 | + |
| 140 | + kindZFS = "ZFS" |
| 141 | + kindZFSThin = "ZFS_THIN" |
| 142 | + kindLVMThin = "LVM_THIN" |
| 143 | +) |
0 commit comments