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

mendersoftware / mender-artifact / 1583872815

11 Dec 2024 11:51AM UTC coverage: 77.145% (-0.02%) from 77.166%
1583872815

Pull #652

gitlab-ci

bahaa-ghazal
chore: Improving error message when calling mender-artifact on a device without mender

Ticket: MEN-7832
Signed-off-by: Bahaa Aldeen Ghazal <bahaa.ghazal@northern.tech>
Pull Request #652: chore: Improving error message

0 of 6 new or added lines in 1 file covered. (0.0%)

18 existing lines in 1 file now uncovered.

5799 of 7517 relevant lines covered (77.15%)

133.18 hits per line

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

59.47
/cli/write.go
1
// Copyright 2023 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

15
package cli
16

17
import (
18
        "bufio"
19
        "context"
20
        "encoding/json"
21
        "fmt"
22
        "os"
23
        "os/exec"
24
        "os/signal"
25
        "regexp"
26
        "strings"
27
        "syscall"
28
        "time"
29

30
        "io"
31
        "io/ioutil"
32

33
        "github.com/pkg/errors"
34
        "github.com/urfave/cli"
35

36
        "github.com/mendersoftware/mender-artifact/artifact"
37
        "github.com/mendersoftware/mender-artifact/artifact/stage"
38
        "github.com/mendersoftware/mender-artifact/awriter"
39
        "github.com/mendersoftware/mender-artifact/cli/util"
40
        "github.com/mendersoftware/mender-artifact/handlers"
41
        "github.com/mendersoftware/mender-artifact/utils"
42
)
43

44
func writeRootfsImageChecksum(rootfsFilename string,
45
        typeInfo *artifact.TypeInfoV3, legacy bool) (err error) {
187✔
46
        chk := artifact.NewWriterChecksum(ioutil.Discard)
187✔
47
        payload, err := os.Open(rootfsFilename)
187✔
48
        if err != nil {
189✔
49
                return cli.NewExitError(
2✔
50
                        fmt.Sprintf("Failed to open the payload file: %q", rootfsFilename),
2✔
51
                        1,
2✔
52
                )
2✔
53
        }
2✔
54
        if _, err = io.Copy(chk, payload); err != nil {
185✔
55
                return cli.NewExitError("Failed to generate the checksum for the payload", 1)
×
56
        }
×
57
        checksum := string(chk.Checksum())
185✔
58

185✔
59
        checksumKey := "rootfs-image.checksum"
185✔
60
        if legacy {
187✔
61
                checksumKey = "rootfs_image_checksum"
2✔
62
        }
2✔
63

64
        Log.Debugf("Adding the `%s`: %q to Artifact provides", checksumKey, checksum)
185✔
65
        if typeInfo == nil {
185✔
66
                return errors.New("Type-info is unitialized")
×
67
        }
×
68
        if typeInfo.ArtifactProvides == nil {
187✔
69
                t, err := artifact.NewTypeInfoProvides(map[string]string{checksumKey: checksum})
2✔
70
                if err != nil {
2✔
71
                        return errors.Wrapf(err, "Failed to write the "+"`"+checksumKey+"` provides")
×
72
                }
×
73
                typeInfo.ArtifactProvides = t
2✔
74
        } else {
183✔
75
                typeInfo.ArtifactProvides[checksumKey] = checksum
183✔
76
        }
183✔
77
        return nil
185✔
78
}
79

80
func validateInput(c *cli.Context) error {
82✔
81
        // Version 2 and 3 validation.
82✔
82
        fileMissing := false
82✔
83
        if c.Command.Name != "bootstrap-artifact" {
160✔
84
                if len(c.String("file")) == 0 {
78✔
85
                        fileMissing = true
×
86
                }
×
87
        }
88
        if len(c.StringSlice("device-type")) == 0 ||
82✔
89
                len(c.String("artifact-name")) == 0 || fileMissing {
82✔
90
                return cli.NewExitError(
×
91
                        "must provide `device-type`, `artifact-name` and `file`",
×
92
                        errArtifactInvalidParameters,
×
93
                )
×
94
        }
×
95
        if len(strings.Fields(c.String("artifact-name"))) > 1 {
84✔
96
                // check for whitespace in artifact-name
2✔
97
                return cli.NewExitError(
2✔
98
                        "whitespace is not allowed in the artifact-name",
2✔
99
                        errArtifactInvalidParameters,
2✔
100
                )
2✔
101
        }
2✔
102
        return nil
80✔
103
}
104

105
func createRootfsFromSSH(c *cli.Context) (string, error) {
×
106
        rootfsFilename, err := getDeviceSnapshot(c)
×
107
        if err != nil {
×
108
                return rootfsFilename, cli.NewExitError("SSH error: "+err.Error(), 1)
×
109
        }
×
110

111
        // check for blkid and get filesystem type
112
        fstype, err := imgFilesystemType(rootfsFilename)
×
113
        if err != nil {
×
114
                if err == errBlkidNotFound {
×
115
                        Log.Warnf("Skipping running fsck on the Artifact: %v", err)
×
116
                        return rootfsFilename, nil
×
117
                }
×
118
                return rootfsFilename, cli.NewExitError(
×
119
                        "imgFilesystemType error: "+err.Error(),
×
120
                        errArtifactCreate,
×
121
                )
×
122
        }
123

124
        // run fsck
125
        switch fstype {
×
126
        case fat:
×
127
                err = runFsck(rootfsFilename, "vfat")
×
128
        case ext:
×
129
                err = runFsck(rootfsFilename, "ext4")
×
130
        case unsupported:
×
131
                err = errors.New("createRootfsFromSSH: unsupported filesystem")
×
132

133
        }
134
        if err != nil {
×
135
                return rootfsFilename, cli.NewExitError("runFsck error: "+err.Error(), errArtifactCreate)
×
136
        }
×
137

138
        return rootfsFilename, nil
×
139
}
140

141
func makeEmptyUpdates(ctx *cli.Context) (*awriter.Updates, error) {
4✔
142
        handler := handlers.NewBootstrapArtifact()
4✔
143

4✔
144
        dataFiles := make([](*handlers.DataFile), 0)
4✔
145
        if err := handler.SetUpdateFiles(dataFiles); err != nil {
4✔
146
                return nil, cli.NewExitError(
×
147
                        err,
×
148
                        1,
×
149
                )
×
150
        }
×
151

152
        upd := &awriter.Updates{
4✔
153
                Updates: []handlers.Composer{handler},
4✔
154
        }
4✔
155
        return upd, nil
4✔
156
}
157

158
func writeBootstrapArtifact(c *cli.Context) error {
4✔
159
        comp, err := artifact.NewCompressorFromId(c.GlobalString("compression"))
4✔
160
        if err != nil {
4✔
161
                return cli.NewExitError(
×
162
                        "compressor '"+c.GlobalString("compression")+"' is not supported: "+err.Error(),
×
163
                        1,
×
164
                )
×
165
        }
×
166

167
        if err := validateInput(c); err != nil {
4✔
168
                Log.Error(err.Error())
×
169
                return err
×
170
        }
×
171

172
        // set the default name
173
        name := "artifact.mender"
4✔
174
        if len(c.String("output-path")) > 0 {
8✔
175
                name = c.String("output-path")
4✔
176
        }
4✔
177
        version := c.Int("version")
4✔
178

4✔
179
        Log.Debugf("creating bootstrap artifact [%s], version: %d", name, version)
4✔
180

4✔
181
        var w io.Writer
4✔
182
        if name == "-" {
4✔
183
                w = os.Stdout
×
184
        } else {
4✔
185
                f, err := os.Create(name)
4✔
186
                if err != nil {
4✔
187
                        return cli.NewExitError(
×
188
                                "can not create bootstrap artifact file: "+err.Error(),
×
189
                                errArtifactCreate,
×
190
                        )
×
191
                }
×
192
                defer f.Close()
4✔
193
                w = f
4✔
194
        }
195

196
        aw, err := artifactWriter(c, comp, w, version)
4✔
197
        if err != nil {
4✔
198
                return cli.NewExitError(err.Error(), 1)
×
199
        }
×
200

201
        depends := artifact.ArtifactDepends{
4✔
202
                ArtifactName:      c.StringSlice("artifact-name-depends"),
4✔
203
                CompatibleDevices: c.StringSlice("device-type"),
4✔
204
                ArtifactGroup:     c.StringSlice("depends-groups"),
4✔
205
        }
4✔
206

4✔
207
        provides := artifact.ArtifactProvides{
4✔
208
                ArtifactName:  c.String("artifact-name"),
4✔
209
                ArtifactGroup: c.String("provides-group"),
4✔
210
        }
4✔
211

4✔
212
        upd, err := makeEmptyUpdates(c)
4✔
213
        if err != nil {
4✔
214
                return err
×
215
        }
×
216

217
        typeInfoV3, _, err := makeTypeInfo(c)
4✔
218
        if err != nil {
4✔
219
                return err
×
220
        }
×
221

222
        if !c.Bool("no-progress") {
8✔
223
                ctx, cancel := context.WithCancel(context.Background())
4✔
224
                go reportProgress(ctx, aw.State)
4✔
225
                defer cancel()
4✔
226
                aw.ProgressWriter = utils.NewProgressWriter()
4✔
227
        }
4✔
228

229
        err = aw.WriteArtifact(
4✔
230
                &awriter.WriteArtifactArgs{
4✔
231
                        Format:     "mender",
4✔
232
                        Version:    version,
4✔
233
                        Devices:    c.StringSlice("device-type"),
4✔
234
                        Name:       c.String("artifact-name"),
4✔
235
                        Updates:    upd,
4✔
236
                        Scripts:    nil,
4✔
237
                        Depends:    &depends,
4✔
238
                        Provides:   &provides,
4✔
239
                        TypeInfoV3: typeInfoV3,
4✔
240
                        Bootstrap:  true,
4✔
241
                })
4✔
242
        if err != nil {
4✔
243
                return cli.NewExitError(err.Error(), 1)
×
244
        }
×
245
        return nil
4✔
246
}
247

248
func writeRootfs(c *cli.Context) error {
78✔
249
        comp, err := artifact.NewCompressorFromId(c.GlobalString("compression"))
78✔
250
        if err != nil {
78✔
251
                return cli.NewExitError(
×
252
                        "compressor '"+c.GlobalString("compression")+"' is not supported: "+err.Error(),
×
253
                        1,
×
254
                )
×
255
        }
×
256

257
        if err := validateInput(c); err != nil {
80✔
258
                Log.Error(err.Error())
2✔
259
                return err
2✔
260
        }
2✔
261

262
        // set the default name
263
        name := "artifact.mender"
76✔
264
        if len(c.String("output-path")) > 0 {
152✔
265
                name = c.String("output-path")
76✔
266
        }
76✔
267
        version := c.Int("version")
76✔
268

76✔
269
        Log.Debugf("creating artifact [%s], version: %d", name, version)
76✔
270
        rootfsFilename := c.String("file")
76✔
271
        if strings.HasPrefix(rootfsFilename, "ssh://") {
76✔
272
                rootfsFilename, err = createRootfsFromSSH(c)
×
273
                defer os.Remove(rootfsFilename)
×
274
                if err != nil {
×
275
                        return cli.NewExitError(err.Error(), errArtifactCreate)
×
276
                }
×
277
        }
278

279
        var h handlers.Composer
76✔
280
        switch version {
76✔
281
        case 2:
2✔
282
                h = handlers.NewRootfsV2(rootfsFilename)
2✔
283
        case 3:
68✔
284
                h = handlers.NewRootfsV3(rootfsFilename)
68✔
285
        default:
6✔
286
                return cli.NewExitError(
6✔
287
                        fmt.Sprintf("Artifact version %d is not supported", version),
6✔
288
                        errArtifactUnsupportedVersion,
6✔
289
                )
6✔
290
        }
291

292
        upd := &awriter.Updates{
70✔
293
                Updates: []handlers.Composer{h},
70✔
294
        }
70✔
295

70✔
296
        var w io.Writer
70✔
297
        if name == "-" {
70✔
298
                w = os.Stdout
×
299
        } else {
70✔
300
                f, err := os.Create(name)
70✔
301
                if err != nil {
70✔
302
                        return cli.NewExitError(
×
303
                                "can not create artifact file: "+err.Error(),
×
304
                                errArtifactCreate,
×
305
                        )
×
306
                }
×
307
                defer f.Close()
70✔
308
                w = f
70✔
309
        }
310

311
        aw, err := artifactWriter(c, comp, w, version)
70✔
312
        if err != nil {
72✔
313
                return cli.NewExitError(err.Error(), 1)
2✔
314
        }
2✔
315

316
        scr, err := scripts(c.StringSlice("script"))
68✔
317
        if err != nil {
70✔
318
                return cli.NewExitError(err.Error(), 1)
2✔
319
        }
2✔
320

321
        depends := artifact.ArtifactDepends{
66✔
322
                ArtifactName:      c.StringSlice("artifact-name-depends"),
66✔
323
                CompatibleDevices: c.StringSlice("device-type"),
66✔
324
                ArtifactGroup:     c.StringSlice("depends-groups"),
66✔
325
        }
66✔
326

66✔
327
        provides := artifact.ArtifactProvides{
66✔
328
                ArtifactName:  c.String("artifact-name"),
66✔
329
                ArtifactGroup: c.String("provides-group"),
66✔
330
        }
66✔
331

66✔
332
        typeInfoV3, _, err := makeTypeInfo(c)
66✔
333
        if err != nil {
66✔
334
                return err
×
335
        }
×
336

337
        if !c.Bool("no-checksum-provide") {
128✔
338
                legacy := c.Bool("legacy-rootfs-image-checksum")
62✔
339
                if err = writeRootfsImageChecksum(rootfsFilename, typeInfoV3, legacy); err != nil {
62✔
340
                        return cli.NewExitError(
×
341
                                errors.Wrap(err, "Failed to write the `rootfs-image.checksum` to the artifact"),
×
342
                                1,
×
343
                        )
×
344
                }
×
345
        }
346

347
        if !c.Bool("no-progress") {
132✔
348
                ctx, cancel := context.WithCancel(context.Background())
66✔
349
                go reportProgress(ctx, aw.State)
66✔
350
                defer cancel()
66✔
351
                aw.ProgressWriter = utils.NewProgressWriter()
66✔
352
        }
66✔
353

354
        err = aw.WriteArtifact(
66✔
355
                &awriter.WriteArtifactArgs{
66✔
356
                        Format:     "mender",
66✔
357
                        Version:    version,
66✔
358
                        Devices:    c.StringSlice("device-type"),
66✔
359
                        Name:       c.String("artifact-name"),
66✔
360
                        Updates:    upd,
66✔
361
                        Scripts:    scr,
66✔
362
                        Depends:    &depends,
66✔
363
                        Provides:   &provides,
66✔
364
                        TypeInfoV3: typeInfoV3,
66✔
365
                })
66✔
366
        if err != nil {
66✔
367
                return cli.NewExitError(err.Error(), 1)
×
368
        }
×
369
        return nil
66✔
370
}
371

372
func reportProgress(c context.Context, state chan string) {
70✔
373
        fmt.Fprintln(os.Stderr, "Writing Artifact...")
70✔
374
        str := fmt.Sprintf("%-20s\t", <-state)
70✔
375
        fmt.Fprint(os.Stderr, str)
70✔
376
        for {
418✔
377
                select {
348✔
378
                case str = <-state:
278✔
379
                        if str == stage.Data {
348✔
380
                                fmt.Fprintf(os.Stderr, "\033[1;32m\u2713\033[0m\n")
70✔
381
                                fmt.Fprintln(os.Stderr, "Payload")
70✔
382
                        } else {
278✔
383
                                fmt.Fprintf(os.Stderr, "\033[1;32m\u2713\033[0m\n")
208✔
384
                                str = fmt.Sprintf("%-20s\t", str)
208✔
385
                                fmt.Fprint(os.Stderr, str)
208✔
386
                        }
208✔
387
                case <-c.Done():
70✔
388
                        return
70✔
389
                }
390
        }
391
}
392

393
func artifactWriter(c *cli.Context, comp artifact.Compressor, w io.Writer,
394
        ver int) (*awriter.Writer, error) {
125✔
395
        privateKey, err := getKey(c)
125✔
396
        if err != nil {
127✔
397
                return nil, err
2✔
398
        }
2✔
399
        if privateKey != nil {
133✔
400
                if ver == 0 {
10✔
401
                        // check if we are having correct version
×
402
                        return nil, errors.New("can not use signed artifact with version 0")
×
403
                }
×
404
                return awriter.NewWriterSigned(w, comp, privateKey), nil
10✔
405
        }
406
        return awriter.NewWriter(w, comp), nil
113✔
407
}
408

409
func makeUpdates(ctx *cli.Context) (*awriter.Updates, error) {
51✔
410
        version := ctx.Int("version")
51✔
411

51✔
412
        var handler, augmentHandler handlers.Composer
51✔
413
        switch version {
51✔
414
        case 2:
×
415
                return nil, cli.NewExitError(
×
416
                        "Module images need at least artifact format version 3",
×
417
                        errArtifactInvalidParameters)
×
418
        case 3:
51✔
419
                handler = handlers.NewModuleImage(ctx.String("type"))
51✔
420
        default:
×
421
                return nil, cli.NewExitError(
×
422
                        fmt.Sprintf("unsupported artifact version: %v", version),
×
423
                        errArtifactUnsupportedVersion,
×
424
                )
×
425
        }
426

427
        dataFiles := make([](*handlers.DataFile), 0, len(ctx.StringSlice("file")))
51✔
428
        for _, file := range ctx.StringSlice("file") {
105✔
429
                dataFiles = append(dataFiles, &handlers.DataFile{Name: file})
54✔
430
        }
54✔
431
        if err := handler.SetUpdateFiles(dataFiles); err != nil {
51✔
432
                return nil, cli.NewExitError(
×
433
                        err,
×
434
                        1,
×
435
                )
×
436
        }
×
437

438
        upd := &awriter.Updates{
51✔
439
                Updates: []handlers.Composer{handler},
51✔
440
        }
51✔
441

51✔
442
        if ctx.String("augment-type") != "" {
55✔
443
                augmentHandler = handlers.NewAugmentedModuleImage(handler, ctx.String("augment-type"))
4✔
444
                dataFiles = make([](*handlers.DataFile), 0, len(ctx.StringSlice("augment-file")))
4✔
445
                for _, file := range ctx.StringSlice("augment-file") {
8✔
446
                        dataFiles = append(dataFiles, &handlers.DataFile{Name: file})
4✔
447
                }
4✔
448
                if err := augmentHandler.SetUpdateAugmentFiles(dataFiles); err != nil {
4✔
449
                        return nil, cli.NewExitError(
×
450
                                err,
×
451
                                1,
×
452
                        )
×
453
                }
×
454
                upd.Augments = []handlers.Composer{augmentHandler}
4✔
455
        }
456

457
        return upd, nil
51✔
458
}
459

460
// makeTypeInfo returns the type-info provides and depends and the augmented
461
// type-info provides and depends, or nil.
462
func makeTypeInfo(ctx *cli.Context) (*artifact.TypeInfoV3, *artifact.TypeInfoV3, error) {
121✔
463
        // Make key value pairs from the type-info fields supplied on command
121✔
464
        // line.
121✔
465
        var keyValues *map[string]string
121✔
466

121✔
467
        var typeInfoDepends artifact.TypeInfoDepends
121✔
468
        keyValues, err := extractKeyValues(ctx.StringSlice("depends"))
121✔
469
        if err != nil {
121✔
470
                return nil, nil, err
×
471
        } else if keyValues != nil {
151✔
472
                if typeInfoDepends, err = artifact.NewTypeInfoDepends(*keyValues); err != nil {
30✔
473
                        return nil, nil, err
×
474
                }
×
475
        }
476

477
        var typeInfoProvides artifact.TypeInfoProvides
121✔
478
        keyValues, err = extractKeyValues(ctx.StringSlice("provides"))
121✔
479
        if err != nil {
121✔
480
                return nil, nil, err
×
481
        } else if keyValues != nil {
153✔
482
                if typeInfoProvides, err = artifact.NewTypeInfoProvides(*keyValues); err != nil {
32✔
483
                        return nil, nil, err
×
484
                }
×
485
        }
486
        typeInfoProvides = applySoftwareVersionToTypeInfoProvides(ctx, typeInfoProvides)
121✔
487

121✔
488
        var augmentTypeInfoDepends artifact.TypeInfoDepends
121✔
489
        keyValues, err = extractKeyValues(ctx.StringSlice("augment-depends"))
121✔
490
        if err != nil {
121✔
491
                return nil, nil, err
×
492
        } else if keyValues != nil {
125✔
493
                if augmentTypeInfoDepends, err = artifact.NewTypeInfoDepends(*keyValues); err != nil {
4✔
494
                        return nil, nil, err
×
495
                }
×
496
        }
497

498
        var augmentTypeInfoProvides artifact.TypeInfoProvides
121✔
499
        keyValues, err = extractKeyValues(ctx.StringSlice("augment-provides"))
121✔
500
        if err != nil {
121✔
501
                return nil, nil, err
×
502
        } else if keyValues != nil {
125✔
503
                if augmentTypeInfoProvides, err = artifact.NewTypeInfoProvides(*keyValues); err != nil {
4✔
504
                        return nil, nil, err
×
505
                }
×
506
        }
507

508
        clearsArtifactProvides, err := makeClearsArtifactProvides(ctx)
121✔
509
        if err != nil {
121✔
510
                return nil, nil, err
×
511
        }
×
512

513
        var typeInfo *string
121✔
514
        if ctx.Command.Name != "bootstrap-artifact" {
238✔
515
                typeFlag := ctx.String("type")
117✔
516
                typeInfo = &typeFlag
117✔
517
        }
117✔
518
        typeInfoV3 := &artifact.TypeInfoV3{
121✔
519
                Type:                   typeInfo,
121✔
520
                ArtifactDepends:        typeInfoDepends,
121✔
521
                ArtifactProvides:       typeInfoProvides,
121✔
522
                ClearsArtifactProvides: clearsArtifactProvides,
121✔
523
        }
121✔
524

121✔
525
        if ctx.String("augment-type") == "" {
238✔
526
                // Non-augmented artifact
117✔
527
                if len(ctx.StringSlice("augment-file")) != 0 ||
117✔
528
                        len(ctx.StringSlice("augment-depends")) != 0 ||
117✔
529
                        len(ctx.StringSlice("augment-provides")) != 0 ||
117✔
530
                        ctx.String("augment-meta-data") != "" {
117✔
531

×
532
                        err = errors.New("Must give --augment-type argument if making augmented artifact")
×
533
                        fmt.Println(err.Error())
×
534
                        return nil, nil, err
×
535
                }
×
536
                return typeInfoV3, nil, nil
117✔
537
        }
538

539
        augmentType := ctx.String("augment-type")
4✔
540
        augmentTypeInfoV3 := &artifact.TypeInfoV3{
4✔
541
                Type:             &augmentType,
4✔
542
                ArtifactDepends:  augmentTypeInfoDepends,
4✔
543
                ArtifactProvides: augmentTypeInfoProvides,
4✔
544
        }
4✔
545

4✔
546
        return typeInfoV3, augmentTypeInfoV3, nil
4✔
547
}
548

549
func getSoftwareVersion(
550
        artifactName,
551
        softwareFilesystem,
552
        softwareName,
553
        softwareNameDefault,
554
        softwareVersion string,
555
        noDefaultSoftwareVersion bool,
556
) map[string]string {
135✔
557
        result := map[string]string{}
135✔
558
        softwareVersionName := "rootfs-image"
135✔
559
        if softwareFilesystem != "" {
151✔
560
                softwareVersionName = softwareFilesystem
16✔
561
        }
16✔
562
        if !noDefaultSoftwareVersion {
242✔
563
                if softwareName == "" {
202✔
564
                        softwareName = softwareNameDefault
95✔
565
                }
95✔
566
                if softwareVersion == "" {
202✔
567
                        softwareVersion = artifactName
95✔
568
                }
95✔
569
        }
570
        if softwareName != "" {
186✔
571
                softwareVersionName += fmt.Sprintf(".%s", softwareName)
51✔
572
        }
51✔
573
        if softwareVersionName != "" && softwareVersion != "" {
246✔
574
                result[softwareVersionName+".version"] = softwareVersion
111✔
575
        }
111✔
576
        return result
135✔
577
}
578

579
// applySoftwareVersionToTypeInfoProvides returns a new mapping, enriched with provides
580
// for the software version; the mapping provided as argument is not modified
581
func applySoftwareVersionToTypeInfoProvides(
582
        ctx *cli.Context,
583
        typeInfoProvides artifact.TypeInfoProvides,
584
) artifact.TypeInfoProvides {
121✔
585
        result := make(map[string]string)
121✔
586
        for key, value := range typeInfoProvides {
185✔
587
                result[key] = value
64✔
588
        }
64✔
589
        artifactName := ctx.String("artifact-name")
121✔
590
        softwareFilesystem := ctx.String(softwareFilesystemFlag)
121✔
591
        softwareName := ctx.String(softwareNameFlag)
121✔
592
        softwareNameDefault := ""
121✔
593
        if ctx.Command.Name == "module-image" {
172✔
594
                softwareNameDefault = ctx.String("type")
51✔
595
        }
51✔
596
        if ctx.Command.Name == "bootstrap-artifact" {
125✔
597
                return result
4✔
598
        }
4✔
599
        softwareVersion := ctx.String(softwareVersionFlag)
117✔
600
        noDefaultSoftwareVersion := ctx.Bool(noDefaultSoftwareVersionFlag)
117✔
601
        if softwareVersionMapping := getSoftwareVersion(
117✔
602
                artifactName,
117✔
603
                softwareFilesystem,
117✔
604
                softwareName,
117✔
605
                softwareNameDefault,
117✔
606
                softwareVersion,
117✔
607
                noDefaultSoftwareVersion,
117✔
608
        ); len(softwareVersionMapping) > 0 {
214✔
609
                for key, value := range softwareVersionMapping {
194✔
610
                        if result[key] == "" || softwareVersionOverridesProvides(ctx, key) {
190✔
611
                                result[key] = value
93✔
612
                        }
93✔
613
                }
614
        }
615
        return result
117✔
616
}
617

618
func softwareVersionOverridesProvides(ctx *cli.Context, key string) bool {
6✔
619
        mainCtx := ctx.Parent().Parent()
6✔
620
        cmdLine := strings.Join(mainCtx.Args(), " ")
6✔
621

6✔
622
        var providesVersion string = `(-p|--provides)(\s+|=)` + regexp.QuoteMeta(key) + ":"
6✔
623
        reProvidesVersion := regexp.MustCompile(providesVersion)
6✔
624
        providesIndexes := reProvidesVersion.FindAllStringIndex(cmdLine, -1)
6✔
625

6✔
626
        var softareVersion string = "--software-(name|version|filesystem)"
6✔
627
        reSoftwareVersion := regexp.MustCompile(softareVersion)
6✔
628
        softwareIndexes := reSoftwareVersion.FindAllStringIndex(cmdLine, -1)
6✔
629

6✔
630
        if len(providesIndexes) == 0 {
6✔
631
                return true
×
632
        } else if len(softwareIndexes) == 0 {
8✔
633
                return false
2✔
634
        } else {
6✔
635
                return softwareIndexes[len(softwareIndexes)-1][0] >
4✔
636
                        providesIndexes[len(providesIndexes)-1][0]
4✔
637
        }
4✔
638
}
639

640
func makeClearsArtifactProvides(ctx *cli.Context) ([]string, error) {
121✔
641
        list := ctx.StringSlice(clearsProvidesFlag)
121✔
642

121✔
643
        if ctx.Bool(noDefaultClearsProvidesFlag) ||
121✔
644
                ctx.Bool(noDefaultSoftwareVersionFlag) ||
121✔
645
                ctx.Command.Name == "bootstrap-artifact" {
153✔
646
                return list, nil
32✔
647
        }
32✔
648

649
        var softwareFilesystem string
89✔
650
        if ctx.IsSet("software-filesystem") {
97✔
651
                softwareFilesystem = ctx.String("software-filesystem")
8✔
652
        } else {
89✔
653
                softwareFilesystem = "rootfs-image"
81✔
654
        }
81✔
655

656
        var softwareName string
89✔
657
        if len(ctx.String("software-name")) > 0 {
97✔
658
                softwareName = ctx.String("software-name") + "."
8✔
659
        } else if ctx.Command.Name == "rootfs-image" {
141✔
660
                softwareName = ""
52✔
661
                // "rootfs_image_checksum" is included for legacy
52✔
662
                // reasons. Previously, "rootfs_image_checksum" was the name
52✔
663
                // given to the checksum, but new artifacts follow the new dot
52✔
664
                // separated scheme, "rootfs-image.checksum", which also has the
52✔
665
                // correct dash instead of the incorrect underscore.
52✔
666
                //
52✔
667
                // "artifact_group" is included as a sane default for
52✔
668
                // rootfs-image updates. A standard rootfs-image update should
52✔
669
                // clear the group if it does not have one.
52✔
670
                if softwareFilesystem == "rootfs-image" {
102✔
671
                        list = append(list, "artifact_group", "rootfs_image_checksum")
50✔
672
                }
50✔
673
        } else if ctx.Command.Name == "module-image" {
58✔
674
                softwareName = ctx.String("type") + "."
29✔
675
        } else {
29✔
676
                return nil, errors.New(
×
677
                        "Unknown write command in makeClearsArtifactProvides(), this is a bug.",
×
678
                )
×
679
        }
×
680

681
        defaultCap := fmt.Sprintf("%s.%s*", softwareFilesystem, softwareName)
89✔
682
        for _, cap := range list {
195✔
683
                if defaultCap == cap {
108✔
684
                        // Avoid adding it twice if the default is the same as a
2✔
685
                        // specified provide.
2✔
686
                        goto dontAdd
2✔
687
                }
688
        }
689
        list = append(list, defaultCap)
87✔
690

87✔
691
dontAdd:
87✔
692
        return list, nil
89✔
693
}
694

695
func makeMetaData(ctx *cli.Context) (map[string]interface{}, map[string]interface{}, error) {
92✔
696
        var metaData map[string]interface{}
92✔
697
        var augmentMetaData map[string]interface{}
92✔
698

92✔
699
        if len(ctx.String("meta-data")) > 0 {
113✔
700
                file, err := os.Open(ctx.String("meta-data"))
21✔
701
                if err != nil {
21✔
702
                        return metaData, augmentMetaData, cli.NewExitError(err, errArtifactInvalidParameters)
×
703
                }
×
704
                defer file.Close()
21✔
705
                dec := json.NewDecoder(file)
21✔
706
                err = dec.Decode(&metaData)
21✔
707
                if err != nil {
21✔
708
                        return metaData, augmentMetaData, cli.NewExitError(err, errArtifactInvalidParameters)
×
709
                }
×
710
        }
711

712
        if len(ctx.String("augment-meta-data")) > 0 {
96✔
713
                file, err := os.Open(ctx.String("augment-meta-data"))
4✔
714
                if err != nil {
4✔
715
                        return metaData, augmentMetaData, cli.NewExitError(err, errArtifactInvalidParameters)
×
716
                }
×
717
                defer file.Close()
4✔
718
                dec := json.NewDecoder(file)
4✔
719
                err = dec.Decode(&augmentMetaData)
4✔
720
                if err != nil {
4✔
721
                        return metaData, augmentMetaData, cli.NewExitError(err, errArtifactInvalidParameters)
×
722
                }
×
723
        }
724

725
        return metaData, augmentMetaData, nil
92✔
726
}
727

728
func writeModuleImage(ctx *cli.Context) error {
51✔
729
        comp, err := artifact.NewCompressorFromId(ctx.GlobalString("compression"))
51✔
730
        if err != nil {
51✔
731
                return cli.NewExitError(
×
732
                        "compressor '"+ctx.GlobalString("compression")+"' is not supported: "+err.Error(),
×
733
                        1,
×
734
                )
×
735
        }
×
736

737
        // set the default name
738
        name := "artifact.mender"
51✔
739
        if len(ctx.String("output-path")) > 0 {
102✔
740
                name = ctx.String("output-path")
51✔
741
        }
51✔
742
        version := ctx.Int("version")
51✔
743

51✔
744
        if version == 1 {
51✔
745
                return cli.NewExitError("Mender-Artifact version 1 is not supported", 1)
×
746
        }
×
747

748
        // The device-type flag is required
749
        if len(ctx.StringSlice("device-type")) == 0 {
51✔
750
                return cli.NewExitError("The `device-type` flag is required", 1)
×
751
        }
×
752

753
        upd, err := makeUpdates(ctx)
51✔
754
        if err != nil {
51✔
755
                return err
×
756
        }
×
757

758
        var w io.Writer
51✔
759
        if name == "-" {
51✔
760
                w = os.Stdout
×
761
        } else {
51✔
762
                f, err := os.Create(name)
51✔
763
                if err != nil {
51✔
764
                        return cli.NewExitError(
×
765
                                "can not create artifact file: "+err.Error(),
×
766
                                errArtifactCreate,
×
767
                        )
×
768
                }
×
769
                defer f.Close()
51✔
770
                w = f
51✔
771
        }
772

773
        aw, err := artifactWriter(ctx, comp, w, version)
51✔
774
        if err != nil {
51✔
775
                return cli.NewExitError(err.Error(), 1)
×
776
        }
×
777

778
        scr, err := scripts(ctx.StringSlice("script"))
51✔
779
        if err != nil {
51✔
780
                return cli.NewExitError(err.Error(), 1)
×
781
        }
×
782

783
        depends := artifact.ArtifactDepends{
51✔
784
                ArtifactName:      ctx.StringSlice("artifact-name-depends"),
51✔
785
                CompatibleDevices: ctx.StringSlice("device-type"),
51✔
786
                ArtifactGroup:     ctx.StringSlice("depends-groups"),
51✔
787
        }
51✔
788

51✔
789
        provides := artifact.ArtifactProvides{
51✔
790
                ArtifactName:  ctx.String("artifact-name"),
51✔
791
                ArtifactGroup: ctx.String("provides-group"),
51✔
792
        }
51✔
793

51✔
794
        typeInfoV3, augmentTypeInfoV3, err := makeTypeInfo(ctx)
51✔
795
        if err != nil {
51✔
796
                return err
×
797
        }
×
798

799
        metaData, augmentMetaData, err := makeMetaData(ctx)
51✔
800
        if err != nil {
51✔
801
                return err
×
802
        }
×
803

804
        err = aw.WriteArtifact(
51✔
805
                &awriter.WriteArtifactArgs{
51✔
806
                        Format:            "mender",
51✔
807
                        Version:           version,
51✔
808
                        Devices:           ctx.StringSlice("device-type"),
51✔
809
                        Name:              ctx.String("artifact-name"),
51✔
810
                        Updates:           upd,
51✔
811
                        Scripts:           scr,
51✔
812
                        Depends:           &depends,
51✔
813
                        Provides:          &provides,
51✔
814
                        TypeInfoV3:        typeInfoV3,
51✔
815
                        MetaData:          metaData,
51✔
816
                        AugmentTypeInfoV3: augmentTypeInfoV3,
51✔
817
                        AugmentMetaData:   augmentMetaData,
51✔
818
                })
51✔
819
        if err != nil {
51✔
820
                return cli.NewExitError(err.Error(), 1)
×
821
        }
×
822
        return nil
51✔
823
}
824

825
func extractKeyValues(params []string) (*map[string]string, error) {
648✔
826
        var keyValues *map[string]string
648✔
827
        if len(params) > 0 {
724✔
828
                keyValues = &map[string]string{}
76✔
829
                for _, arg := range params {
232✔
830
                        split := strings.SplitN(arg, ":", 2)
156✔
831
                        if len(split) != 2 {
156✔
832
                                return nil, cli.NewExitError(
×
833
                                        fmt.Sprintf("argument must have a delimiting colon: %s", arg),
×
834
                                        errArtifactInvalidParameters)
×
835
                        }
×
836
                        (*keyValues)[split[0]] = split[1]
156✔
837
                }
838
        }
839
        return keyValues, nil
648✔
840
}
841

842
// SSH to remote host and dump rootfs snapshot to a local temporary file.
843
func getDeviceSnapshot(c *cli.Context) (string, error) {
×
844

×
845
        const sshInitMagic = "Initializing snapshot..."
×
846
        var userAtHost string
×
847
        var sigChan chan os.Signal
×
848
        var errChan chan error
×
849
        ctx, cancel := context.WithCancel(context.Background())
×
850
        defer cancel()
×
851
        port := "22"
×
852
        host := strings.TrimPrefix(c.String("file"), "ssh://")
×
853

×
854
        if remotePort := strings.Split(host, ":"); len(remotePort) == 2 {
×
855
                port = remotePort[1]
×
856
                userAtHost = remotePort[0]
×
857
        } else {
×
858
                userAtHost = host
×
859
        }
×
860

861
        // Prepare command-line arguments
862
        args := c.StringSlice("ssh-args")
×
863
        // Check if port is specified explicitly with the --ssh-args flag
×
864
        addPort := true
×
865
        for _, arg := range args {
×
866
                if strings.Contains(arg, "-p") {
×
867
                        addPort = false
×
868
                        break
×
869
                }
870
        }
871
        if addPort {
×
872
                args = append(args, "-p", port)
×
873
        }
×
874
        args = append(args, userAtHost)
×
875
        // First echo to stdout such that we know when ssh connection is
×
876
        // established (password prompt is written to /dev/tty directly,
×
877
        // and hence impossible to detect).
×
878
        // When user id is 0 do not bother with sudo.
×
879
        args = append(
×
880
                args,
×
881
                "/bin/sh",
×
882
                "-c",
×
NEW
883
                `'[ $(id -u) -eq 0 ] || sudo_cmd="sudo -S"`+
×
NEW
884
                        `; if which mender-snapshot 1> /dev/null`+
×
NEW
885
                        `; then $sudo_cmd /bin/sh -c "echo `+sshInitMagic+`; mender-snapshot dump" | cat`+
×
NEW
886
                        `; elif which mender 1> /dev/null`+
×
NEW
887
                        `; then $sudo_cmd /bin/sh -c "echo `+sshInitMagic+`; mender snapshot dump" | cat`+
×
NEW
888
                        `; else echo "Mender not found: Please check that Mender is installed" >&2 && exit 1; fi'`,
×
889
        )
×
890

×
891
        cmd := exec.Command("ssh", args...)
×
892

×
893
        // Simply connect stdin/stderr
×
894
        cmd.Stdin = os.Stdin
×
895
        cmd.Stderr = os.Stderr
×
896
        stdout, err := cmd.StdoutPipe()
×
897
        if err != nil {
×
898
                return "", errors.New("Error redirecting stdout on exec")
×
899
        }
×
900

901
        // Create tempfile for storing the snapshot
UNCOV
902
        f, err := ioutil.TempFile("", "rootfs.tmp")
×
903
        if err != nil {
×
904
                return "", err
×
905
        }
×
906
        filePath := f.Name()
×
907

×
908
        defer removeOnPanic(filePath)
×
909
        defer f.Close()
×
910

×
911
        // Disable tty echo before starting
×
912
        term, err := util.DisableEcho(int(os.Stdin.Fd()))
×
913
        if err == nil {
×
914
                sigChan = make(chan os.Signal, 1)
×
915
                errChan = make(chan error, 1)
×
916
                // Make sure that echo is enabled if the process gets
×
917
                // interrupted
×
918
                signal.Notify(sigChan)
×
919
                go util.EchoSigHandler(ctx, sigChan, errChan, term)
×
920
        } else if err != syscall.ENOTTY {
×
921
                return "", err
×
922
        }
×
923

UNCOV
924
        if err := cmd.Start(); err != nil {
×
925
                return "", err
×
926
        }
×
927

928
        // Wait for 60 seconds for ssh to establish connection
UNCOV
929
        err = waitForBufferSignal(stdout, os.Stdout, sshInitMagic, 2*time.Minute)
×
930
        if err != nil {
×
931
                _ = cmd.Process.Kill()
×
932
                return "", errors.Wrap(err,
×
933
                        "Error waiting for ssh session to be established.")
×
934
        }
×
935

UNCOV
936
        _, err = recvSnapshot(f, stdout)
×
937
        if err != nil {
×
938
                _ = cmd.Process.Kill()
×
939
                return "", err
×
940
        }
×
941

UNCOV
942
        if cmd.ProcessState != nil && cmd.ProcessState.Exited() {
×
943
                return "", errors.New("SSH session closed unexpectedly")
×
944
        }
×
945

UNCOV
946
        if err = cmd.Wait(); err != nil {
×
947
                return "", errors.Wrap(err,
×
948
                        "SSH session closed with error")
×
949
        }
×
950

UNCOV
951
        if sigChan != nil {
×
952
                // Wait for signal handler to execute
×
953
                signal.Stop(sigChan)
×
954
                cancel()
×
955
                err = <-errChan
×
956
        }
×
957

UNCOV
958
        return filePath, err
×
959
}
960

961
// Reads from src waiting for the string specified by signal, writing all other
962
// output appearing at src to sink. The function returns an error if occurs
963
// reading from the stream or the deadline exceeds.
964
func waitForBufferSignal(src io.Reader, sink io.Writer,
UNCOV
965
        signal string, deadline time.Duration) error {
×
966

×
967
        var err error
×
968
        errChan := make(chan error)
×
969

×
970
        go func() {
×
971
                stdoutRdr := bufio.NewReader(src)
×
972
                for {
×
973
                        line, err := stdoutRdr.ReadString('\n')
×
974
                        if err != nil {
×
975
                                errChan <- err
×
976
                                break
×
977
                        }
UNCOV
978
                        if strings.Contains(line, signal) {
×
979
                                errChan <- nil
×
980
                                break
×
981
                        }
UNCOV
982
                        _, err = sink.Write([]byte(line + "\n"))
×
983
                        if err != nil {
×
984
                                errChan <- err
×
985
                                break
×
986
                        }
987
                }
988
        }()
989

UNCOV
990
        select {
×
991
        case err = <-errChan:
×
992
                // Error from goroutine
UNCOV
993
        case <-time.After(deadline):
×
994
                err = errors.New("Input deadline exceeded")
×
995
        }
UNCOV
996
        return err
×
997
}
998

999
// Performs the same operation as io.Copy while at the same time prining
1000
// the number of bytes written at any time.
UNCOV
1001
func recvSnapshot(dst io.Writer, src io.Reader) (int64, error) {
×
1002
        buf := make([]byte, 1024*1024*32)
×
1003
        var written int64
×
1004
        for {
×
1005
                nr, err := src.Read(buf)
×
1006
                if err == io.EOF {
×
1007
                        break
×
1008
                } else if err != nil {
×
1009
                        return written, errors.Wrap(err,
×
1010
                                "Error receiving snapshot from device")
×
1011
                }
×
1012
                nw, err := dst.Write(buf[:nr])
×
1013
                if err != nil {
×
1014
                        return written, errors.Wrap(err,
×
1015
                                "Error storing snapshot locally")
×
1016
                } else if nw < nr {
×
1017
                        return written, io.ErrShortWrite
×
1018
                }
×
1019
                written += int64(nw)
×
1020
        }
UNCOV
1021
        return written, nil
×
1022
}
1023

UNCOV
1024
func removeOnPanic(filename string) {
×
1025
        if r := recover(); r != nil {
×
1026
                err := os.Remove(filename)
×
1027
                if err != nil {
×
1028
                        switch v := r.(type) {
×
1029
                        case string:
×
1030
                                err = errors.Wrap(errors.New(v), err.Error())
×
1031
                                panic(err)
×
1032
                        case error:
×
1033
                                err = errors.Wrap(v, err.Error())
×
1034
                                panic(err)
×
1035
                        default:
×
1036
                                panic(r)
×
1037
                        }
1038
                }
UNCOV
1039
                panic(r)
×
1040
        }
1041
}
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