• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender / 4c96a297b9f0ccc0923a19448cadbe6c11570b9c

pending completion
4c96a297b9f0ccc0923a19448cadbe6c11570b9c

push

gitlab-ci

GitHub
Merge pull request #1209 from merlin-northern/men_5915_lat_lon_in_inv_geo

9806 of 12349 relevant lines covered (79.41%)

30.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.11
/cli/snapshot.go
1
// Copyright 2021 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14
package cli
15

16
import (
17
        "compress/gzip"
18
        "fmt"
19
        "io"
20
        "os"
21
        "os/signal"
22
        "time"
23

24
        log "github.com/sirupsen/logrus"
25

26
        "github.com/pkg/errors"
27
        "github.com/urfave/cli/v2"
28
        "golang.org/x/sys/unix"
29

30
        "github.com/mendersoftware/mender/system"
31
        "github.com/mendersoftware/mender/utils"
32
)
33

34
const (
35
        // Error messages //
36
        errMsgDataPartFmt = "Device-local data is stored on the partition " +
37
                "being snapshotted: %s. The recommended approach is to have " +
38
                "a separate data-partition mounted on \"/data\" and add a " +
39
                "symbolic link (%s -> /data). Consult either " +
40
                "https://docs.mender.io/system-updates-debian-family/overview#partition-layout" +
41
                "for Debian family or " +
42
                "https://docs.mender.io/system-updates-yocto-project/overview#partition-layout" +
43
                "in case you use Yocto"
44
        errMsgThawFmt = "Try running `fsfreeze -u %s` or press `SYSRQ+j`, " +
45
                "immediately!"
46

47
        // Watchdog constants //
48
        // Ping from main thread, keep the operation alive.
49
        wdtPing = 1
50
        // Exit request from the main thread. `response` can be read exactly
51
        // once, after this.
52
        wdtExit = 2
53
        // wdtIntervalSec Sets the polling interval of the listening routine.
54
        // NOTE: this should be sufficient enough for the user to for example
55
        //       type their password to ssh etc.
56
        wdtIntervalSec = 30
57

58
        // bufferSize for the Copy function
59
        bufferSize = 32 * 1024
60
)
61

62
type snapshot struct {
63
        src io.ReadCloser
64
        dst io.WriteCloser
65
        // The FIFREEZE ioctl requires that the file descriptor points to a
66
        // directory
67
        freezeDir *os.File
68
        // Watchdog used to detect and release "freeze deadlocks"
69
        wdt *watchDog
70
        // Optional progressbar.
71
        pb *utils.ProgressWriter
72
}
73

74
type watchDog struct {
75
        request  chan int
76
        response chan error
77
}
78

79
func newWatchDog() *watchDog {
9✔
80
        return &watchDog{
9✔
81
                request:  make(chan int),
9✔
82
                response: make(chan error),
9✔
83
        }
9✔
84
}
9✔
85

86
// DumpSnapshot copies a snapshot of the root filesystem to stdout.
87
func (runOpts *runOptionsType) DumpSnapshot(ctx *cli.Context) error {
10✔
88

10✔
89
        log.SetOutput(os.Stderr)
10✔
90

10✔
91
        fd := int(os.Stdout.Fd())
10✔
92
        // Make sure stdout is redirected (not a tty device)
10✔
93
        if _, err := unix.IoctlGetTermios(fd, unix.TCGETS); err == nil {
10✔
94
                return errDumpTerminal
×
95
        }
×
96

97
        return CopySnapshot(ctx, os.Stdout)
10✔
98
}
99

100
// CopySnapshot freezes the filesystem and copies a snapshot to out.
101
func CopySnapshot(ctx *cli.Context, dst io.WriteCloser) error {
10✔
102
        var err error
10✔
103
        srcPath := ctx.String("source")
10✔
104
        ss := &snapshot{dst: dst}
10✔
105
        defer ss.cleanup()
10✔
106

10✔
107
        // Ensure we don't write logs to the filesystem
10✔
108
        log.SetOutput(os.Stderr)
10✔
109
        if ctx.Bool("quiet") {
10✔
110
                log.SetLevel(log.ErrorLevel)
×
111
        }
×
112

113
        srcID, err := ss.validateSrcDev(srcPath, ctx.String("data"))
10✔
114
        if err != nil {
10✔
115
                return err
×
116
        }
×
117

118
        srcPath, err = system.GetBlockDeviceFromID(srcID)
10✔
119
        if err != nil {
10✔
120
                return err
×
121
        }
×
122

123
        err = ss.init(srcPath, ctx.String("compression"), !ctx.Bool("quiet"))
10✔
124
        if err != nil {
10✔
125
                return err
×
126
        }
×
127

128
        srcDev, err := system.GetMountInfoFromDeviceID(srcID)
10✔
129
        if err == system.ErrDevNotMounted {
11✔
130
                // If not mounted we're good to go
1✔
131
        } else if err != nil {
10✔
132
                return errors.Wrap(err, "failed preconditions for snapshot")
×
133
        } else {
9✔
134
                if ss.freezeDir, err = os.OpenFile(
9✔
135
                        srcDev.MountPoint, 0, 0); err == nil {
18✔
136
                        // freezeHandler is a transparent signal handler that
9✔
137
                        // ensures system.ThawFs is called upon a terminating
9✔
138
                        // signal.
9✔
139
                        ss.wdt = newWatchDog()
9✔
140
                        fh := freezeHandler{
9✔
141
                                fd:  ss.freezeDir,
9✔
142
                                wdt: ss.wdt,
9✔
143
                        }
9✔
144
                        go fh.run()
9✔
145
                        log.Debugf("Freezing %s", srcDev.MountPoint)
9✔
146
                        err = system.FreezeFS(int(ss.freezeDir.Fd()))
9✔
147
                }
9✔
148
                if err != nil {
9✔
149
                        log.Warnf("Failed to freeze filesystem on %s: %s",
×
150
                                srcDev.MountSource, err.Error())
×
151
                        log.Warn("The snapshot might become invalid.")
×
152
                }
×
153
        }
154

155
        err = ss.Do()
10✔
156
        if err != nil {
12✔
157
                return err
2✔
158
        }
2✔
159
        return nil
8✔
160
}
161

162
// init the input/output and optionally a progressbar for snapshot.
163
func (ss *snapshot) init(
164
        srcPath string,
165
        compression string,
166
        showProgress bool,
167
) error {
10✔
168
        var fdSrc *os.File
10✔
169
        var err error
10✔
170

10✔
171
        if fdSrc, err = os.Open(srcPath); err != nil {
10✔
172
                return err
×
173
        }
×
174
        ss.src = fdSrc
10✔
175

10✔
176
        if showProgress {
20✔
177
                // Initialize progress bar
10✔
178
                // Get file system size
10✔
179
                fsSize, err := system.GetBlockDeviceSize(fdSrc)
10✔
180
                if err != nil {
10✔
181
                        return errors.Wrap(err,
×
182
                                "unable to get partition size")
×
183
                }
×
184

185
                ss.pb = utils.NewProgressWriter(int64(fsSize))
10✔
186
        }
187

188
        if err := ss.assignCompression(compression); err != nil {
10✔
189
                return err
×
190
        }
×
191

192
        return err
10✔
193
}
194

195
// assignCompression parses the compression argument and wraps the output
196
// writer.
197
func (ss *snapshot) assignCompression(compression string) error {
10✔
198
        var err error
10✔
199

10✔
200
        switch compression {
10✔
201
        case "none":
9✔
202

203
        case "gzip":
1✔
204
                ss.dst = gzip.NewWriter(ss.dst)
1✔
205

206
        case "lzma":
×
207
                err = errors.New("lzma compression is not implemented for " +
×
208
                        "snapshot command")
×
209

210
        default:
×
211
                err = errors.Errorf("Unknown compression '%s'", compression)
×
212

213
        }
214
        return err
10✔
215
}
216

217
// cleanup closes all open files and stops the freezeHandler if running.
218
func (ss *snapshot) cleanup() {
10✔
219
        // Close open file descriptors
10✔
220
        if ss.dst != nil {
20✔
221
                ss.dst.Close()
10✔
222
        }
10✔
223
        if ss.src != nil {
20✔
224
                ss.src.Close()
10✔
225
        }
10✔
226
        if ss.freezeDir != nil {
19✔
227
                ss.freezeDir.Close()
9✔
228
        }
9✔
229
}
230

231
// validateSrcDev checks that the device id associated with srcPath is valid and
232
// returns the device id [2]uint32{major, minor} number and an error upon
233
// failure.
234
func (ss *snapshot) validateSrcDev(
235
        srcPath string,
236
        dataPath string,
237
) ([2]uint32, error) {
10✔
238
        var err error
10✔
239
        var stat unix.Stat_t
10✔
240

10✔
241
        err = unix.Stat(srcPath, &stat)
10✔
242
        if err != nil {
10✔
243
                return [2]uint32{0, 0}, errors.Wrapf(err,
×
244
                        "failed to validate source %s", srcPath)
×
245
        }
×
246

247
        if (stat.Mode & (unix.S_IFBLK | unix.S_IFDIR)) == 0 {
10✔
248
                return [2]uint32{0, 0}, errors.New(
×
249
                        "source must point to a directory or block device")
×
250
        }
×
251

252
        rootDevID, err := system.GetDeviceIDFromPath(srcPath)
10✔
253
        if err != nil {
10✔
254
                return rootDevID, errors.Wrapf(err,
×
255
                        "error getting device id from path '%s'", srcPath)
×
256
        }
×
257
        dataDevID, err := system.GetDeviceIDFromPath(dataPath)
10✔
258
        if err != nil {
10✔
259
                return rootDevID, errors.Wrapf(err,
×
260
                        "error getting device id from path '%s'", dataPath)
×
261
        }
×
262

263
        if dataDevID == rootDevID {
10✔
264
                log.Errorf(errMsgDataPartFmt, dataPath, dataPath)
×
265
                return rootDevID, errors.Errorf(
×
266
                        "data store (%s) is located on filesystem %s",
×
267
                        dataPath, srcPath)
×
268
        }
×
269
        if rootDevID[0] == 0 {
10✔
270
                log.Errorf("Resolved an unnamed target device (device id: "+
×
271
                        "%d:%d). Is the device running overlayfs? Try "+
×
272
                        "passing the device file to the --source flag",
×
273
                        rootDevID[0], rootDevID[1])
×
274
                return rootDevID, fmt.Errorf(
×
275
                        "unnamed target device (device id: %d:%d)",
×
276
                        rootDevID[0], rootDevID[1])
×
277
        }
×
278

279
        return rootDevID, err
10✔
280
}
281

282
// freezeHandler is a transparent signal handler and watchdog timer ensuring
283
// system.ThawFS is called on a terminating signal before relaying the signal to
284
// the system default handler. The sigChan should be notified on ALL incomming
285
// signals to the process, signals that are ignored by default are also ignored
286
// by this handler. The handler must be periodically pinged using the
287
// wdt.request and wdtPing or its timer will expire, which will trigger an error
288
// as the response, and calling ThawFS. wdtExit must be used to exit the
289
// handler.
290

291
type freezeHandler struct {
292
        fd          *os.File
293
        wdt         *watchDog
294
        thawed      bool
295
        timerActive bool
296
        sigChan     chan os.Signal
297
        retErr      error
298
        timer       *time.Timer
299
        exit        bool
300
}
301

302
func (fh *freezeHandler) run() {
9✔
303
        fh.timer = time.NewTimer(wdtIntervalSec * time.Second)
9✔
304
        fh.sigChan = make(chan os.Signal, 1)
9✔
305
        signal.Notify(fh.sigChan)
9✔
306
        fh.thawed = false
9✔
307
        fh.timerActive = true
9✔
308

9✔
309
        defer func() {
18✔
310
                signal.Stop(fh.sigChan)
9✔
311
                signal.Reset()
9✔
312
                close(fh.sigChan)
9✔
313
                if fh.timerActive && !fh.timer.Stop() {
9✔
314
                        <-fh.timer.C
×
315
                }
×
316
        }()
317

318
        for !fh.exit {
18✔
319
                select {
9✔
320
                case request := <-fh.wdt.request:
9✔
321
                        fh.handleRequest(request)
9✔
322

323
                case <-fh.timer.C:
2✔
324
                        fh.handleExpiredTimer()
2✔
325

326
                case sig := <-fh.sigChan:
7✔
327
                        fh.handleSignal(sig)
7✔
328
                }
329
        }
330
}
331

332
func (fh *freezeHandler) handleRequest(request int) {
9✔
333
        if request == wdtExit {
18✔
334
                if !fh.thawed {
16✔
335
                        thawFs(fh.fd)
7✔
336
                }
7✔
337
                fh.exit = true
9✔
338

339
        } else if request == wdtPing {
18✔
340
                if fh.timerActive {
17✔
341
                        if !fh.timer.Stop() {
8✔
342
                                <-fh.timer.C
×
343
                        }
×
344
                        fh.timer.Reset(wdtIntervalSec * time.Second)
8✔
345
                }
346
        }
347
        fh.wdt.response <- fh.retErr
9✔
348
}
349

350
func (fh *freezeHandler) handleExpiredTimer() {
2✔
351
        fh.timerActive = false
2✔
352
        if fh.thawed {
2✔
353
                return
×
354
        }
×
355
        errStr := "Freeze timer expired due to " +
2✔
356
                "blocked main process."
2✔
357
        log.Error(errStr)
2✔
358
        log.Info("Unfreezing filesystem")
2✔
359
        thawFs(fh.fd)
2✔
360
        fh.retErr = errors.New(errStr)
2✔
361
        fh.thawed = true
2✔
362
}
363

364
func (fh *freezeHandler) handleSignal(sig os.Signal) {
7✔
365
        if fh.thawed {
7✔
366
                return
×
367
        }
×
368
        log.Debugf("Freeze handler received signal: %s",
7✔
369
                sig.String())
7✔
370
        if sig == unix.SIGURG ||
7✔
371
                sig == unix.SIGWINCH ||
7✔
372
                sig == unix.SIGCHLD {
14✔
373
                // Signals that are ignored by default
7✔
374
                // keep ignoring them.
7✔
375
                return
7✔
376
        }
7✔
377
        thawFs(fh.fd)
×
378
        fh.retErr = errors.Errorf("Freeze handler interrupted by signal: %s",
×
379
                sig.String())
×
380
        fh.thawed = true
×
381
}
382

383
func thawFs(fd *os.File) {
9✔
384
        log.Debugf("Thawing %s", fd.Name())
9✔
385
        if err := system.ThawFS(int(fd.Fd())); err != nil {
9✔
386
                log.Errorf("Unable to unfreeze filesystem: %s",
×
387
                        err.Error())
×
388
                log.Errorf(errMsgThawFmt, fd.Name())
×
389
        }
×
390
}
391

392
// Do starts copying data from src to dst and conditionally updates the
393
// progressbar.
394
func (ss *snapshot) Do() (retErr error) {
10✔
395
        defer func() {
20✔
396
                if ss.wdt != nil {
19✔
397
                        ss.wdt.request <- wdtExit
9✔
398
                        err := <-ss.wdt.response
9✔
399
                        if retErr == nil {
16✔
400
                                retErr = err
7✔
401
                        }
7✔
402
                }
403
        }()
404

405
        buf := make([]byte, bufferSize)
10✔
406
        for {
20✔
407
                n, err := copyChunk(buf, ss.src, ss.dst)
10✔
408
                if err == io.EOF {
18✔
409
                        break
8✔
410
                } else if n < 0 {
10✔
411
                        break
×
412
                } else if err != nil {
10✔
413
                        return err
×
414
                }
×
415
                if ss.wdt != nil {
19✔
416
                        ss.wdt.request <- wdtPing
9✔
417
                        err = <-ss.wdt.response
9✔
418
                }
9✔
419
                if err != nil {
12✔
420
                        return err
2✔
421
                }
2✔
422
                if ss.pb != nil {
18✔
423
                        ss.pb.Tick(uint64(n))
9✔
424
                }
9✔
425
        }
426
        return nil
8✔
427
}
428

429
func copyChunk(buf []byte, src io.Reader, dst io.Writer) (int, error) {
10✔
430
        n, err := src.Read(buf)
10✔
431
        if err != nil {
18✔
432
                return n, err
8✔
433
        } else if n < 0 {
18✔
434
                return n, fmt.Errorf("source returned negative number of bytes")
×
435
        }
×
436

437
        w, err := dst.Write(buf[:n])
10✔
438
        if err != nil {
10✔
439
                return w, err
×
440
        } else if w < n {
10✔
441
                err = errors.Wrap(io.ErrShortWrite,
×
442
                        "Error writing to stream")
×
443
                return w, err
×
444
        }
×
445
        return w, nil
10✔
446
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc