master
go 323 lines 8.96 KB
Raw
1 package commands
2
3 import (
4 "encoding/hex"
5 "errors"
6 "fmt"
7 "io"
8 "time"
9
10 "github.com/ipfs/boxo/path"
11 cid "github.com/ipfs/go-cid"
12 "github.com/ipfs/go-datastore"
13 "github.com/ipfs/go-datastore/mount"
14 "github.com/ipfs/go-datastore/query"
15 cmds "github.com/ipfs/go-ipfs-cmds"
16 oldcmds "github.com/ipfs/kubo/commands"
17 "github.com/ipfs/kubo/core/commands/cmdenv"
18 node "github.com/ipfs/kubo/core/node"
19 "github.com/ipfs/kubo/core/shutdown"
20 fsrepo "github.com/ipfs/kubo/repo/fsrepo"
21 )
22
23 // diagHealthyProbeCIDStr is the well-known empty UnixFS directory,
24 // built into every kubo node. Fetching it succeeds regardless of peers,
25 // DHT, or user content, so it isolates the DAG/blockstore pipeline.
26 const diagHealthyProbeCIDStr = "QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn"
27
28 var DiagCmd = &cmds.Command{
29 Helptext: cmds.HelpText{
30 Tagline: "Generate diagnostic reports.",
31 },
32
33 Subcommands: map[string]*cmds.Command{
34 "sys": sysDiagCmd,
35 "cmds": ActiveReqsCmd,
36 "profile": sysProfileCmd,
37 "datastore": diagDatastoreCmd,
38 "healthy": diagHealthyCmd,
39 },
40 }
41
42 // diagHealthyCmd is a container-healthcheck probe. It fails when shutdown
43 // has been initiated (even if the RPC API still answers) or when the DAG
44 // pipeline cannot resolve a built-in CID.
45 var diagHealthyCmd = &cmds.Command{
46 Helptext: cmds.HelpText{
47 Tagline: "Report whether the daemon is operational.",
48 ShortDescription: `
49 Exits 0 if the daemon is running and can resolve the well-known empty
50 UnixFS directory. Exits non-zero if shutdown has started or the DAG
51 pipeline is broken. Intended for container healthchecks.
52 `,
53 },
54 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
55 if t := shutdown.StartedAt(); !t.IsZero() {
56 return fmt.Errorf("daemon is shutting down (started %s ago)", time.Since(t).Round(time.Second))
57 }
58 api, err := cmdenv.GetApi(env, req)
59 if err != nil {
60 return err
61 }
62 probeCID, err := cid.Decode(diagHealthyProbeCIDStr)
63 if err != nil {
64 return fmt.Errorf("invalid probe CID: %w", err)
65 }
66 if _, _, err := api.ResolvePath(req.Context, path.FromCid(probeCID)); err != nil {
67 return fmt.Errorf("probe resolve: %w", err)
68 }
69 if _, err := api.Dag().Get(req.Context, probeCID); err != nil {
70 return fmt.Errorf("probe fetch: %w", err)
71 }
72 return cmds.EmitOnce(res, "ok")
73 },
74 }
75
76 var diagDatastoreCmd = &cmds.Command{
77 Status: cmds.Experimental,
78 Helptext: cmds.HelpText{
79 Tagline: "Low-level datastore inspection for debugging and testing.",
80 ShortDescription: `
81 'ipfs diag datastore' provides low-level access to the datastore for debugging
82 and testing purposes.
83
84 WARNING: FOR DEBUGGING/TESTING ONLY
85
86 These commands expose internal datastore details and should not be used
87 in production workflows. The datastore format may change between versions.
88
89 The daemon must not be running when calling these commands.
90
91 When the provider keystore datastores exist on disk (nodes with
92 Provide.DHT.SweepEnabled=true), they are automatically mounted into the
93 datastore view under /provider/keystore/0/ and /provider/keystore/1/.
94
95 EXAMPLES
96
97 Inspecting pubsub seqno validator state:
98
99 $ ipfs diag datastore count /pubsub/seqno/
100 2
101 $ ipfs diag datastore get --hex /pubsub/seqno/12D3KooW...
102 Key: /pubsub/seqno/12D3KooW...
103 Hex Dump:
104 00000000 18 81 81 c8 91 c0 ea f6 |........|
105
106 Writing a test key (debugging only):
107
108 $ ipfs diag datastore put /test/mykey "hello"
109
110 Inspecting provider keystore (requires SweepEnabled):
111
112 $ ipfs diag datastore count /provider/keystore/0/
113 $ ipfs diag datastore count /provider/keystore/1/
114 `,
115 },
116 Subcommands: map[string]*cmds.Command{
117 "get": diagDatastoreGetCmd,
118 "put": diagDatastorePutCmd,
119 "count": diagDatastoreCountCmd,
120 },
121 }
122
123 const diagDatastoreHexOptionName = "hex"
124
125 type diagDatastoreGetResult struct {
126 Key string `json:"key"`
127 Value []byte `json:"value"`
128 HexDump string `json:"hex_dump,omitempty"`
129 }
130
131 // openDiagDatastore opens the repo datastore and conditionally mounts any
132 // provider keystore datastores that exist on disk. It returns the composite
133 // datastore and a cleanup function that must be called when done.
134 func openDiagDatastore(env cmds.Environment) (datastore.Datastore, func(), error) {
135 cctx := env.(*oldcmds.Context)
136 repo, err := fsrepo.Open(cctx.ConfigRoot)
137 if err != nil {
138 return nil, nil, fmt.Errorf("failed to open repo: %w", err)
139 }
140
141 extraMounts, extraCloser, err := node.MountKeystoreDatastores(repo)
142 if err != nil {
143 repo.Close()
144 return nil, nil, err
145 }
146
147 closer := func() {
148 extraCloser()
149 repo.Close()
150 }
151
152 if len(extraMounts) == 0 {
153 return repo.Datastore(), closer, nil
154 }
155
156 mounts := []mount.Mount{{Prefix: datastore.NewKey("/"), Datastore: repo.Datastore()}}
157 mounts = append(mounts, extraMounts...)
158 return mount.New(mounts), closer, nil
159 }
160
161 var diagDatastoreGetCmd = &cmds.Command{
162 Status: cmds.Experimental,
163 Helptext: cmds.HelpText{
164 Tagline: "Read a raw key from the datastore.",
165 ShortDescription: `
166 Returns the value stored at the given datastore key.
167 Default output is raw bytes. Use --hex for human-readable hex dump.
168
169 The daemon must not be running when using this command.
170
171 WARNING: FOR DEBUGGING/TESTING ONLY
172 `,
173 },
174 Arguments: []cmds.Argument{
175 cmds.StringArg("key", true, false, "Datastore key to read (e.g., /pubsub/seqno/<peerid>)"),
176 },
177 Options: []cmds.Option{
178 cmds.BoolOption(diagDatastoreHexOptionName, "Output hex dump instead of raw bytes"),
179 },
180 NoRemote: true,
181 PreRun: DaemonNotRunning,
182 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
183 ds, closer, err := openDiagDatastore(env)
184 if err != nil {
185 return err
186 }
187 defer closer()
188
189 keyStr := req.Arguments[0]
190 key := datastore.NewKey(keyStr)
191
192 val, err := ds.Get(req.Context, key)
193 if err != nil {
194 if errors.Is(err, datastore.ErrNotFound) {
195 return fmt.Errorf("key not found: %s", keyStr)
196 }
197 return fmt.Errorf("failed to read key: %w", err)
198 }
199
200 result := &diagDatastoreGetResult{
201 Key: keyStr,
202 Value: val,
203 }
204
205 if hexDump, _ := req.Options[diagDatastoreHexOptionName].(bool); hexDump {
206 result.HexDump = hex.Dump(val)
207 }
208
209 return cmds.EmitOnce(res, result)
210 },
211 Type: diagDatastoreGetResult{},
212 Encoders: cmds.EncoderMap{
213 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, result *diagDatastoreGetResult) error {
214 if result.HexDump != "" {
215 fmt.Fprintf(w, "Key: %s\nHex Dump:\n%s", result.Key, result.HexDump)
216 return nil
217 }
218 // Raw bytes output
219 _, err := w.Write(result.Value)
220 return err
221 }),
222 },
223 }
224
225 var diagDatastorePutCmd = &cmds.Command{
226 Status: cmds.Experimental,
227 Helptext: cmds.HelpText{
228 Tagline: "Write a raw key-value pair to the datastore.",
229 ShortDescription: `
230 Stores the given value at the specified datastore key.
231
232 The daemon must not be running when using this command.
233
234 WARNING: FOR DEBUGGING/TESTING ONLY
235 `,
236 },
237 Arguments: []cmds.Argument{
238 cmds.StringArg("key", true, false, "Datastore key (e.g., /test/mykey)"),
239 cmds.StringArg("value", true, false, "Value to store (as a string)"),
240 },
241 NoRemote: true,
242 PreRun: DaemonNotRunning,
243 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
244 ds, closer, err := openDiagDatastore(env)
245 if err != nil {
246 return err
247 }
248 defer closer()
249
250 key := datastore.NewKey(req.Arguments[0])
251 if err := ds.Put(req.Context, key, []byte(req.Arguments[1])); err != nil {
252 return fmt.Errorf("failed to put key: %w", err)
253 }
254 if err := ds.Sync(req.Context, key); err != nil {
255 return fmt.Errorf("failed to sync: %w", err)
256 }
257 return nil
258 },
259 }
260
261 type diagDatastoreCountResult struct {
262 Prefix string `json:"prefix"`
263 Count int64 `json:"count"`
264 }
265
266 var diagDatastoreCountCmd = &cmds.Command{
267 Status: cmds.Experimental,
268 Helptext: cmds.HelpText{
269 Tagline: "Count entries matching a datastore prefix.",
270 ShortDescription: `
271 Counts the number of datastore entries whose keys start with the given prefix.
272
273 The daemon must not be running when using this command.
274
275 WARNING: FOR DEBUGGING/TESTING ONLY
276 `,
277 },
278 Arguments: []cmds.Argument{
279 cmds.StringArg("prefix", true, false, "Datastore key prefix (e.g., /pubsub/seqno/)"),
280 },
281 NoRemote: true,
282 PreRun: DaemonNotRunning,
283 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
284 ds, closer, err := openDiagDatastore(env)
285 if err != nil {
286 return err
287 }
288 defer closer()
289
290 prefix := req.Arguments[0]
291
292 q := query.Query{
293 Prefix: prefix,
294 KeysOnly: true,
295 }
296
297 results, err := ds.Query(req.Context, q)
298 if err != nil {
299 return fmt.Errorf("failed to query datastore: %w", err)
300 }
301 defer results.Close()
302
303 var count int64
304 for result := range results.Next() {
305 if result.Error != nil {
306 return fmt.Errorf("query error: %w", result.Error)
307 }
308 count++
309 }
310
311 return cmds.EmitOnce(res, &diagDatastoreCountResult{
312 Prefix: prefix,
313 Count: count,
314 })
315 },
316 Type: diagDatastoreCountResult{},
317 Encoders: cmds.EncoderMap{
318 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, result *diagDatastoreCountResult) error {
319 _, err := fmt.Fprintf(w, "%d\n", result.Count)
320 return err
321 }),
322 },
323 }