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

mendersoftware / mender / 1421288278

21 Aug 2024 09:30AM UTC coverage: 76.154%. First build
1421288278

push

gitlab-ci

kacf
feat: Expand `--stop-after` functionality.

Changelog: Adjust states allowed for the `--stop-after` flag.
These are the allowed states:
* `Download_Leave`
* `ArtifactInstall_Leave`
* `ArtifactInstall_Error`
* `ArtifactCommit` (exits before `ArtifactCommit_Leave`)
* `ArtifactCommit_Error`
* `ArtifactRollback_Leave`
* `ArtifactFailure_Leave`

Changelog: Add `--stop-after` flag to `commit` and `rollback`
commands.

Changelog: Allow `--stop-after` flag to be specified multiple times.

Ticket: MEN-7115

Signed-off-by: Kristian Amlie <kristian.amlie@northern.tech>

145 of 167 new or added lines in 4 files covered. (86.83%)

7377 of 9687 relevant lines covered (76.15%)

11284.59 hits per line

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

70.48
/src/mender-update/cli/cli.cpp
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
#include <mender-update/cli/cli.hpp>
16

17
#ifdef MENDER_EMBED_MENDER_AUTH
18
#include <mender-auth/cli/cli.hpp>
19
#endif
20

21
#include <iostream>
22

23
#include <client_shared/conf.hpp>
24
#include <common/error.hpp>
25
#include <common/expected.hpp>
26

27
namespace mender {
28
namespace update {
29
namespace cli {
30

31
namespace conf = mender::client_shared::conf;
32
namespace error = mender::common::error;
33
namespace expected = mender::common::expected;
34

35
const int NoUpdateInProgressExitStatus = 2;
36
const int RebootExitStatus = 4;
37

38
#ifdef MENDER_EMBED_MENDER_AUTH
39
const conf::CliCommand cmd_auth {
40
        .name = "auth",
41
        .description = "Access built-in mender-auth commands (experimental).",
42
};
43
#endif
44

45
const conf::CliCommand cmd_check_update {
46
        .name = "check-update",
47
        .description = "Force update check",
48
};
49

50
const conf::CliOption opt_stop_after {
51
        conf::CliOption {
52
                .long_option = "stop-after",
53
                .description =
54
                        "Stop after the given state has completed. "
55
                        "Choices are `Download`, `ArtifactInstall` and `ArtifactCommit`. "
56
                        "You can later resume the installation by using the `resume` command. "
57
                        "Note that the client always stops after `ArtifactInstall` if the update module supports rollback.",
58
                .parameter = "STATE",
59
        },
60
};
61

62
const conf::CliCommand cmd_commit {
63
        .name = "commit",
64
        .description = "Commit current Artifact. Returns (2) if no update in progress",
65
        .options_w_values =
66
                {
67
                        opt_stop_after,
68
                },
69
};
70

71
const conf::CliCommand cmd_daemon {
72
        .name = "daemon",
73
        .description = "Start the client as a background service",
74
};
75

76
const conf::CliCommand cmd_install {
77
        .name = "install",
78
        .description = "Mender Artifact to install - local file or a URL",
79
        .argument =
80
                conf::CliArgument {
81
                        .name = "artifact",
82
                        .mandatory = true,
83
                },
84
        .options_w_values =
85
                {
86
                        opt_stop_after,
87
                },
88
        .options =
89
                {
90
                        conf::CliOption {
91
                                .long_option = "reboot-exit-code",
92
                                .description =
93
                                        "Return exit code 4 if a manual reboot is required after the Artifact installation.",
94
                        },
95
                },
96
};
97

98
const conf::CliCommand cmd_resume {
99
        .name = "install",
100
        .description = "Resume an interrupted installation",
101
        .options_w_values =
102
                {
103
                        opt_stop_after,
104
                },
105
        .options =
106
                {
107
                        conf::CliOption {
108
                                .long_option = "reboot-exit-code",
109
                                .description =
110
                                        "Return exit code 4 if a manual reboot is required after the Artifact installation.",
111
                        },
112
                },
113
};
114

115
const conf::CliCommand cmd_rollback {
116
        .name = "rollback",
117
        .description = "Rollback current Artifact. Returns (2) if no update in progress",
118
        .options_w_values =
119
                {
120
                        opt_stop_after,
121
                },
122
};
123

124
const conf::CliCommand cmd_send_inventory {
125
        .name = "send-inventory",
126
        .description = "Force inventory update",
127
};
128

129
const conf::CliCommand cmd_show_artifact {
130
        .name = "show-artifact",
131
        .description = "Print the current artifact name to the command line and exit",
132
};
133

134
const conf::CliCommand cmd_show_provides {
135
        .name = "show-provides",
136
        .description = "Print the current provides to the command line and exit",
137
};
138

139
const conf::CliApp cli_mender_update = {
140
        .name = "mender-update",
141
        .short_description = "manage and start Mender Update",
142
        .long_description =
143
                R"(mender-update integrates both the mender-auth daemon and commands for manually
144
   performing tasks performed by the daemon (see list of COMMANDS below).)",
145
        .commands =
146
                {
147
#ifdef MENDER_EMBED_MENDER_AUTH
148
                        cmd_auth,
149
#endif
150
                        cmd_check_update,
151
                        cmd_commit,
152
                        cmd_daemon,
153
                        cmd_install,
154
                        cmd_resume,
155
                        cmd_rollback,
156
                        cmd_send_inventory,
157
                        cmd_show_artifact,
158
                        cmd_show_provides,
159
                },
160
};
161

162
ExpectedActionPtr ParseUpdateArguments(
130✔
163
        vector<string>::const_iterator start, vector<string>::const_iterator end) {
164
        if (start == end) {
130✔
165
                return expected::unexpected(conf::MakeError(conf::InvalidOptionsError, "Need an action"));
3✔
166
        }
167

168
        bool help_arg = conf::FindCmdlineHelpArg(start + 1, end);
129✔
169
        if (help_arg) {
129✔
170
                conf::PrintCliCommandHelp(cli_mender_update, start[0]);
×
171
                return expected::unexpected(error::MakeError(error::ExitWithSuccessError, ""));
×
172
        }
173

174
        if (start[0] == "show-artifact") {
129✔
175
                conf::CmdlineOptionsIterator iter(start + 1, end, {}, {});
12✔
176
                auto arg = iter.Next();
4✔
177
                if (!arg) {
4✔
178
                        return expected::unexpected(arg.error());
4✔
179
                }
180

181
                return make_shared<ShowArtifactAction>();
4✔
182
        } else if (start[0] == "show-provides") {
125✔
183
                conf::CmdlineOptionsIterator iter(start + 1, end, {}, {});
126✔
184
                auto arg = iter.Next();
42✔
185
                if (!arg) {
42✔
186
                        return expected::unexpected(arg.error());
4✔
187
                }
188

189
                return make_shared<ShowProvidesAction>();
80✔
190
        } else if (start[0] == "install") {
83✔
191
                conf::CmdlineOptionsIterator iter(
192
                        start + 1,
193
                        end,
194
                        conf::CommandOptsSetWithValue(cmd_install.options_w_values),
126✔
195
                        conf::CommandOptsSetWithoutValue(cmd_install.options));
252✔
196
                iter.SetArgumentsMode(conf::ArgumentsMode::AcceptBareArguments);
197

198
                string filename;
199
                bool reboot_exit_code = false;
200
                vector<string> stop_after;
63✔
201
                while (true) {
202
                        auto arg = iter.Next();
131✔
203
                        if (!arg) {
131✔
204
                                return expected::unexpected(arg.error());
2✔
205
                        }
206

207
                        auto value = arg.value();
191✔
208
                        if (value.option == "--reboot-exit-code") {
130✔
209
                                reboot_exit_code = true;
210
                                continue;
7✔
211
                        } else if (value.option == "--stop-after") {
129✔
212
                                if (value.value == "") {
6✔
213
                                        return expected::unexpected(conf::MakeError(
×
214
                                                conf::InvalidOptionsError, "--stop-after needs an argument"));
×
215
                                }
216
                                stop_after.push_back(value.value);
6✔
217
                                continue;
6✔
218
                        } else if (value.option != "") {
123✔
219
                                return expected::unexpected(
×
220
                                        conf::MakeError(conf::InvalidOptionsError, "No such option: " + value.option));
×
221
                        }
222

223
                        if (value.value != "") {
123✔
224
                                if (filename != "") {
62✔
225
                                        return expected::unexpected(conf::MakeError(
1✔
226
                                                conf::InvalidOptionsError, "Too many arguments: " + value.value));
3✔
227
                                } else {
228
                                        filename = value.value;
229
                                }
230
                        } else {
231
                                if (filename == "") {
61✔
232
                                        return expected::unexpected(
1✔
233
                                                conf::MakeError(conf::InvalidOptionsError, "Need a path to an artifact"));
3✔
234
                                } else {
235
                                        break;
236
                                }
237
                        }
238
                }
239

240
                auto install_action = make_shared<InstallAction>(filename);
60✔
241
                install_action->SetRebootExitCode(reboot_exit_code);
242
                install_action->SetStopAfter(std::move(stop_after));
60✔
243
                return install_action;
60✔
244
        } else if (start[0] == "resume") {
20✔
245
                conf::CmdlineOptionsIterator iter(
246
                        start + 1,
247
                        end,
248
                        conf::CommandOptsSetWithValue(cmd_resume.options_w_values),
12✔
249
                        conf::CommandOptsSetWithoutValue(cmd_resume.options));
24✔
250

251
                string filename;
252
                bool reboot_exit_code = false;
253
                vector<string> stop_after;
6✔
254
                while (true) {
255
                        auto arg = iter.Next();
7✔
256
                        if (!arg) {
7✔
257
                                return expected::unexpected(arg.error());
×
258
                        }
259

260
                        auto value = arg.value();
7✔
261
                        if (value.option == "--reboot-exit-code") {
7✔
262
                                reboot_exit_code = true;
263
                                continue;
1✔
264
                        } else if (value.option == "--stop-after") {
7✔
265
                                if (value.value == "") {
1✔
266
                                        return expected::unexpected(conf::MakeError(
×
267
                                                conf::InvalidOptionsError, "--stop-after needs an argument"));
×
268
                                }
269
                                stop_after.push_back(value.value);
1✔
270
                                continue;
1✔
271
                        } else if (value.option != "") {
6✔
272
                                return expected::unexpected(
×
273
                                        conf::MakeError(conf::InvalidOptionsError, "No such option: " + value.option));
×
274
                        }
275

276
                        if (value.value != "") {
6✔
277
                                return expected::unexpected(conf::MakeError(
×
278
                                        conf::InvalidOptionsError, "Too many arguments: " + value.value));
×
279
                        } else {
280
                                break;
281
                        }
282
                }
283

284
                auto resume_action = make_shared<ResumeAction>();
6✔
285
                resume_action->SetRebootExitCode(reboot_exit_code);
286
                resume_action->SetStopAfter(std::move(stop_after));
6✔
287
                return resume_action;
6✔
288
        } else if (start[0] == "commit") {
14✔
289
                conf::CmdlineOptionsIterator iter(
290
                        start + 1, end, conf::CommandOptsSetWithValue(cmd_commit.options_w_values), {});
18✔
291

292
                vector<string> stop_after;
6✔
293
                while (true) {
294
                        auto arg = iter.Next();
8✔
295
                        if (!arg) {
8✔
NEW
296
                                return expected::unexpected(arg.error());
×
297
                        }
298

299
                        auto value = arg.value();
8✔
300
                        if (value.option == "--stop-after") {
8✔
301
                                if (value.value == "") {
2✔
NEW
302
                                        return expected::unexpected(conf::MakeError(
×
NEW
303
                                                conf::InvalidOptionsError, "--stop-after needs an argument"));
×
304
                                }
305
                                stop_after.push_back(value.value);
2✔
306
                                continue;
2✔
307
                        } else if (value.option != "") {
6✔
NEW
308
                                return expected::unexpected(
×
NEW
309
                                        conf::MakeError(conf::InvalidOptionsError, "No such option: " + value.option));
×
310
                        }
311

312
                        if (value.value != "") {
6✔
NEW
313
                                return expected::unexpected(conf::MakeError(
×
NEW
314
                                        conf::InvalidOptionsError, "Too many arguments: " + value.value));
×
315
                        } else {
316
                                break;
317
                        }
318
                }
319

320
                auto commit_action = make_shared<CommitAction>();
6✔
321
                commit_action->SetStopAfter(std::move(stop_after));
6✔
322
                return commit_action;
6✔
323
        } else if (start[0] == "rollback") {
8✔
324
                conf::CmdlineOptionsIterator iter(
325
                        start + 1, end, conf::CommandOptsSetWithValue(cmd_rollback.options_w_values), {});
24✔
326

327
                vector<string> stop_after;
8✔
328
                while (true) {
329
                        auto arg = iter.Next();
9✔
330
                        if (!arg) {
9✔
NEW
331
                                return expected::unexpected(arg.error());
×
332
                        }
333

334
                        auto value = arg.value();
9✔
335
                        if (value.option == "--stop-after") {
9✔
336
                                if (value.value == "") {
1✔
NEW
337
                                        return expected::unexpected(conf::MakeError(
×
NEW
338
                                                conf::InvalidOptionsError, "--stop-after needs an argument"));
×
339
                                }
340
                                stop_after.push_back(value.value);
1✔
341
                                continue;
1✔
342
                        } else if (value.option != "") {
8✔
NEW
343
                                return expected::unexpected(
×
NEW
344
                                        conf::MakeError(conf::InvalidOptionsError, "No such option: " + value.option));
×
345
                        }
346

347
                        if (value.value != "") {
8✔
NEW
348
                                return expected::unexpected(conf::MakeError(
×
NEW
349
                                        conf::InvalidOptionsError, "Too many arguments: " + value.value));
×
350
                        } else {
351
                                break;
352
                        }
353
                }
354

355
                auto rollback_action = make_shared<RollbackAction>();
8✔
356
                rollback_action->SetStopAfter(std::move(stop_after));
8✔
357
                return rollback_action;
8✔
358
        } else if (start[0] == "daemon") {
×
359
                conf::CmdlineOptionsIterator iter(start + 1, end, {}, {});
×
360
                auto arg = iter.Next();
×
361
                if (!arg) {
×
362
                        return expected::unexpected(arg.error());
×
363
                }
364

365
                return make_shared<DaemonAction>();
×
366
        } else if (start[0] == "send-inventory") {
×
367
                conf::CmdlineOptionsIterator iter(start + 1, end, {}, {});
×
368
                auto arg = iter.Next();
×
369
                if (!arg) {
×
370
                        return expected::unexpected(arg.error());
×
371
                }
372

373
                return make_shared<SendInventoryAction>();
×
374
        } else if (start[0] == "check-update") {
×
375
                conf::CmdlineOptionsIterator iter(start + 1, end, {}, {});
×
376
                auto arg = iter.Next();
×
377
                if (!arg) {
×
378
                        return expected::unexpected(arg.error());
×
379
                }
380

381
                return make_shared<CheckUpdateAction>();
×
382
        }
383
#ifdef MENDER_EMBED_MENDER_AUTH
384
        // We do not test for this here, because mender-auth has its own Main() function and
385
        // therefore it is inconvenient (it returns int, not an action). So we do it in Main()
386
        // below instead, but semantically it is the same as doing it here.
387
        //
388
        // else if (start[0] == "auth") {
389
        //         ...stuff...
390
#endif
391
        else {
392
                return expected::unexpected(
×
393
                        conf::MakeError(conf::InvalidOptionsError, "No such action: " + start[0]));
×
394
        }
395
}
396

397
static error::Error DoMain(
134✔
398
        const vector<string> &args,
399
        function<void(mender::update::context::MenderContext &ctx)> test_hook) {
400
        mender::client_shared::conf::MenderConfig config;
268✔
401

402
        auto args_pos = config.ProcessCmdlineArgs(args.begin(), args.end(), cli_mender_update);
134✔
403
        if (!args_pos) {
134✔
404
                if (args_pos.error().code != error::MakeError(error::ExitWithSuccessError, "").code) {
4✔
405
                        conf::PrintCliHelp(cli_mender_update);
1✔
406
                }
407
                return args_pos.error();
4✔
408
        }
409

410
        auto action = ParseUpdateArguments(args.begin() + args_pos.value(), args.end());
130✔
411
        if (!action) {
130✔
412
                if (action.error().code != error::MakeError(error::ExitWithSuccessError, "").code) {
8✔
413
                        if (args.size() > 0) {
8✔
414
                                conf::PrintCliCommandHelp(cli_mender_update, args[0]);
8✔
415
                        } else {
416
                                conf::PrintCliHelp(cli_mender_update);
×
417
                        }
418
                }
419
                return action.error();
8✔
420
        }
421

422
        mender::update::context::MenderContext main_context(config);
122✔
423

424
        test_hook(main_context);
122✔
425

426
        auto err = main_context.Initialize();
122✔
427
        if (error::NoError != err) {
122✔
428
                return err;
×
429
        }
430

431
        return action.value()->Execute(main_context);
122✔
432
}
433

434
int Main(
134✔
435
        const vector<string> &args,
436
        function<void(mender::update::context::MenderContext &ctx)> test_hook) {
437
#ifdef MENDER_EMBED_MENDER_AUTH
438
        // Early special treatment for "auth" argument.
439
        if (args.size() > 0 and args[0] == "auth") {
440
                return mender::auth::cli::Main({args.begin() + 1, args.end()});
441
        }
442
#endif
443

444
        auto err = DoMain(args, test_hook);
134✔
445

446
        if (err.code == context::MakeError(context::NoUpdateInProgressError, "").code) {
134✔
447
                return NoUpdateInProgressExitStatus;
448
        } else if (err.code == context::MakeError(context::RebootRequiredError, "").code) {
132✔
449
                return RebootExitStatus;
450
        } else if (err != error::NoError) {
131✔
451
                if (err.code == error::MakeError(error::ExitWithSuccessError, "").code) {
36✔
452
                        return 0;
453
                } else if (err.code != error::MakeError(error::ExitWithFailureError, "").code) {
33✔
454
                        cerr << "Could not fulfill request: " + err.String() << endl;
62✔
455
                }
456
                return 1;
33✔
457
        }
458

459
        return 0;
460
}
461

462
} // namespace cli
463
} // namespace update
464
} // namespace mender
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

© 2026 Coveralls, Inc