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

mendersoftware / mender / 1636068196

22 Jan 2025 01:07PM UTC coverage: 75.959% (+0.03%) from 75.933%
1636068196

push

gitlab-ci

web-flow
Merge pull request #1731 from lluiscampos/QA-827-mender-artifact-deb

QA-827: Switch to `mender-artifact` deb package

7384 of 9721 relevant lines covered (75.96%)

11125.61 hits per line

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

77.39
/src/mender-update/daemon/states.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/daemon/states.hpp>
16

17
#include <client_shared/conf.hpp>
18
#include <common/events_io.hpp>
19
#include <common/log.hpp>
20
#include <common/path.hpp>
21

22
#include <mender-update/daemon/context.hpp>
23
#include <mender-update/inventory.hpp>
24

25
namespace mender {
26
namespace update {
27
namespace daemon {
28

29
namespace conf = mender::client_shared::conf;
30
namespace error = mender::common::error;
31
namespace events = mender::common::events;
32
namespace kv_db = mender::common::key_value_database;
33
namespace path = mender::common::path;
34
namespace log = mender::common::log;
35

36
namespace main_context = mender::update::context;
37
namespace inventory = mender::update::inventory;
38

39
class DefaultStateHandler {
40
public:
41
        void operator()(const error::Error &err) {
295✔
42
                if (err != error::NoError) {
295✔
43
                        log::Error(err.String());
23✔
44
                        poster.PostEvent(StateEvent::Failure);
23✔
45
                        return;
23✔
46
                }
47
                poster.PostEvent(StateEvent::Success);
272✔
48
        }
49

50
        sm::EventPoster<StateEvent> &poster;
51
};
52

53
static void DefaultAsyncErrorHandler(sm::EventPoster<StateEvent> &poster, const error::Error &err) {
413✔
54
        if (err != error::NoError) {
413✔
55
                log::Error(err.String());
×
56
                poster.PostEvent(StateEvent::Failure);
×
57
        }
58
}
413✔
59

60
void EmptyState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
148✔
61
        // Keep this state truly empty.
62
}
148✔
63

64
void InitState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
57✔
65
        // I will never run - just a placeholder to start the state-machine at
66
        poster.PostEvent(StateEvent::Started); // Start the state machine
57✔
67
}
57✔
68

69
void StateScriptState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
1,036✔
70
        string state_name {script_executor::Name(this->state_, this->action_)};
1,036✔
71
        log::Debug("Executing the  " + state_name + " State Scripts...");
2,072✔
72
        auto err = this->script_.AsyncRunScripts(
73
                this->state_,
74
                this->action_,
75
                [state_name, &poster](error::Error err) {
7,821✔
76
                        if (err != error::NoError) {
1,036✔
77
                                log::Error(
21✔
78
                                        "Received error: (" + err.String() + ") when running the State Script scripts "
42✔
79
                                        + state_name);
63✔
80
                                poster.PostEvent(StateEvent::Failure);
21✔
81
                                return;
21✔
82
                        }
83
                        log::Debug("Successfully ran the " + state_name + " State Scripts...");
2,030✔
84
                        poster.PostEvent(StateEvent::Success);
1,015✔
85
                },
86
                this->on_error_);
2,072✔
87

88
        if (err != error::NoError) {
1,036✔
89
                log::Error(
×
90
                        "Failed to schedule the state script execution for: " + state_name
×
91
                        + " got error: " + err.String());
×
92
                poster.PostEvent(StateEvent::Failure);
×
93
                return;
94
        }
95
}
96

97

98
void SaveStateScriptState::OnEnterSaveState(Context &ctx, sm::EventPoster<StateEvent> &poster) {
278✔
99
        return state_script_state_.OnEnter(ctx, poster);
278✔
100
}
101

102
void IdleState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
116✔
103
        log::Debug("Entering Idle state");
232✔
104
}
116✔
105

106
SubmitInventoryState::SubmitInventoryState(
94✔
107
        events::EventLoop &event_loop, int retry_interval_seconds, int retry_count) :
108
        retry_ {
109
                http::ExponentialBackoff(chrono::seconds(retry_interval_seconds), retry_count),
110
                event_loop} {
188✔
111
}
94✔
112

113
void SubmitInventoryState::HandlePollingError(Context &ctx, sm::EventPoster<StateEvent> &poster) {
×
114
        // When using short polling inteervals, we should adjust the backoff to ensure
115
        // that the intervals do not exceed the maximum retry polling interval, which
116
        // converts the backoff to a fixed interval.
117
        chrono::milliseconds max_interval =
118
                chrono::seconds(ctx.mender_context.GetConfig().retry_poll_interval_seconds);
×
119
        if (max_interval < retry_.backoff.SmallestInterval()) {
×
120
                retry_.backoff.SetSmallestInterval(max_interval);
×
121
                retry_.backoff.SetMaxInterval(max_interval);
×
122
        }
123
        auto exp_interval = retry_.backoff.NextInterval();
×
124
        if (!exp_interval) {
×
125
                log::Debug(
×
126
                        "Not retrying with backoff, retrying InventoryPollIntervalSeconds: "
127
                        + exp_interval.error().String());
×
128
                return;
129
        }
130
        log::Info(
×
131
                "Retrying inventory polling in "
132
                + to_string(chrono::duration_cast<chrono::seconds>(*exp_interval).count()) + " seconds");
×
133

134
        retry_.wait_timer.Cancel();
×
135
        retry_.wait_timer.AsyncWait(*exp_interval, [&poster](error::Error err) {
×
136
                if (err != error::NoError) {
×
137
                        if (err.code != make_error_condition(errc::operation_canceled)) {
×
138
                                log::Error("Retry poll timer caused error: " + err.String());
×
139
                        }
140
                } else {
141
                        poster.PostEvent(StateEvent::InventoryPollingTriggered);
×
142
                }
143
        });
×
144
}
145

146
void SubmitInventoryState::DoSubmitInventory(Context &ctx, sm::EventPoster<StateEvent> &poster) {
57✔
147
        log::Debug("Submitting inventory");
114✔
148

149
        auto handler = [this, &ctx, &poster](error::Error err) {
57✔
150
                if (err != error::NoError) {
57✔
151
                        log::Error("Failed to submit inventory: " + err.String());
×
152
                        // Replace the inventory poll timer with a backoff
153
                        HandlePollingError(ctx, poster);
×
154
                        poster.PostEvent(StateEvent::Failure);
×
155
                        return;
×
156
                }
157
                retry_.backoff.Reset();
158
                ctx.inventory_client->has_submitted_inventory = true;
57✔
159
                poster.PostEvent(StateEvent::Success);
57✔
160
        };
57✔
161

162
        auto err = ctx.inventory_client->PushData(
163
                ctx.mender_context.GetConfig().paths.GetInventoryScriptsDir(),
57✔
164
                ctx.event_loop,
165
                ctx.http_client,
166
                handler);
57✔
167

168
        if (err != error::NoError) {
57✔
169
                // This is the only case the handler won't be called for us by
170
                // PushData() (see inventory::PushInventoryData()).
171
                handler(err);
×
172
        }
173
}
57✔
174

175
void SubmitInventoryState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
57✔
176
        // Schedule timer for next update first, so that long running submissions do not postpone
177
        // the schedule.
178
        log::Debug(
57✔
179
                "Scheduling the next inventory submission in: "
180
                + to_string(ctx.mender_context.GetConfig().inventory_poll_interval_seconds) + " seconds");
114✔
181
        retry_.wait_timer.AsyncWait(
57✔
182
                chrono::seconds(ctx.mender_context.GetConfig().inventory_poll_interval_seconds),
57✔
183
                [&poster](error::Error err) {
2✔
184
                        if (err != error::NoError) {
1✔
185
                                if (err.code != make_error_condition(errc::operation_canceled)) {
×
186
                                        log::Error("Inventory poll timer caused error: " + err.String());
×
187
                                }
188
                        } else {
189
                                poster.PostEvent(StateEvent::InventoryPollingTriggered);
1✔
190
                        }
191
                });
1✔
192

193
        DoSubmitInventory(ctx, poster);
57✔
194
}
57✔
195

196
PollForDeploymentState::PollForDeploymentState(
94✔
197
        events::EventLoop &event_loop, int retry_interval_seconds, int retry_count) :
198
        retry_ {
199
                http::ExponentialBackoff(chrono::seconds(retry_interval_seconds), retry_count),
200
                event_loop} {
188✔
201
}
94✔
202

203
void PollForDeploymentState::HandlePollingError(Context &ctx, sm::EventPoster<StateEvent> &poster) {
×
204
        // When using short polling inteervals, we should adjust the backoff to ensure
205
        // that the intervals do not exceed the maximum retry polling interval, which
206
        // converts the backoff to a fixed interval.
207
        chrono::milliseconds max_interval =
208
                chrono::seconds(ctx.mender_context.GetConfig().retry_poll_interval_seconds);
×
209
        if (max_interval < retry_.backoff.SmallestInterval()) {
×
210
                retry_.backoff.SetSmallestInterval(max_interval);
×
211
                retry_.backoff.SetMaxInterval(max_interval);
×
212
        }
213
        auto exp_interval = retry_.backoff.NextInterval();
×
214
        if (!exp_interval) {
×
215
                log::Debug(
×
216
                        "Not retrying with backoff, retrying with UpdatePollIntervalSeconds: "
217
                        + exp_interval.error().String());
×
218
                return;
219
        }
220
        log::Info(
×
221
                "Retrying deployment polling in "
222
                + to_string(chrono::duration_cast<chrono::seconds>(*exp_interval).count()) + " seconds");
×
223

224
        retry_.wait_timer.Cancel();
×
225
        retry_.wait_timer.AsyncWait(*exp_interval, [&poster](error::Error err) {
×
226
                if (err != error::NoError) {
×
227
                        if (err.code != make_error_condition(errc::operation_canceled)) {
×
228
                                log::Error("Retry poll timer caused error: " + err.String());
×
229
                        }
230
                } else {
231
                        poster.PostEvent(StateEvent::DeploymentPollingTriggered);
×
232
                }
233
        });
×
234
}
235

236
void PollForDeploymentState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
57✔
237
        log::Debug("Polling for update");
114✔
238

239
        // Schedule timer for next update first, so that long running submissions do not postpone
240
        // the schedule.
241
        log::Debug(
57✔
242
                "Scheduling the next deployment check in: "
243
                + to_string(ctx.mender_context.GetConfig().update_poll_interval_seconds) + " seconds");
114✔
244
        retry_.wait_timer.AsyncWait(
57✔
245
                chrono::seconds(ctx.mender_context.GetConfig().update_poll_interval_seconds),
57✔
246
                [&poster](error::Error err) {
4✔
247
                        if (err != error::NoError) {
2✔
248
                                if (err.code != make_error_condition(errc::operation_canceled)) {
×
249
                                        log::Error("Update poll timer caused error: " + err.String());
×
250
                                }
251
                        } else {
252
                                poster.PostEvent(StateEvent::DeploymentPollingTriggered);
2✔
253
                        }
254
                });
59✔
255

256
        auto err = ctx.deployment_client->CheckNewDeployments(
257
                ctx.mender_context,
258
                ctx.http_client,
259
                [this, &ctx, &poster](mender::update::deployments::CheckUpdatesAPIResponse response) {
112✔
260
                        if (!response) {
56✔
261
                                log::Error("Error while polling for deployment: " + response.error().String());
×
262
                                // Replace the update poll timer with a backoff
263
                                HandlePollingError(ctx, poster);
×
264
                                poster.PostEvent(StateEvent::Failure);
×
265
                                return;
1✔
266
                        } else if (!response.value()) {
56✔
267
                                log::Info("No update available");
2✔
268
                                poster.PostEvent(StateEvent::NothingToDo);
1✔
269
                                if (not ctx.inventory_client->has_submitted_inventory) {
1✔
270
                                        // If we have not submitted inventory successfully at least
271
                                        // once, schedule this after receiving a successful response
272
                                        // with no update. This enables inventory to be submitted
273
                                        // immediately after the device has been accepted. If there
274
                                        // is an update available, an inventory update will be
275
                                        // scheduled at the end of it unconditionally.
276
                                        poster.PostEvent(StateEvent::InventoryPollingTriggered);
×
277
                                }
278

279
                                retry_.backoff.Reset();
280
                                return;
1✔
281
                        }
282
                        retry_.backoff.Reset();
283

284
                        auto exp_data = ApiResponseJsonToStateData(response.value().value());
55✔
285
                        if (!exp_data) {
55✔
286
                                log::Error("Error in API response: " + exp_data.error().String());
×
287
                                poster.PostEvent(StateEvent::Failure);
×
288
                                return;
289
                        }
290

291
                        // Make a new set of update data.
292
                        ctx.deployment.state_data.reset(new StateData(std::move(exp_data.value())));
55✔
293

294
                        ctx.BeginDeploymentLogging();
55✔
295

296
                        log::Info("Running Mender client " + conf::kMenderVersion);
110✔
297
                        log::Info(
55✔
298
                                "Deployment with ID " + ctx.deployment.state_data->update_info.id + " started.");
110✔
299

300
                        poster.PostEvent(StateEvent::DeploymentStarted);
55✔
301
                        poster.PostEvent(StateEvent::Success);
55✔
302
                });
57✔
303

304
        if (err != error::NoError) {
57✔
305
                log::Error("Error when trying to poll for deployment: " + err.String());
2✔
306
                poster.PostEvent(StateEvent::Failure);
1✔
307
        }
308
}
57✔
309

310
void SaveState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
534✔
311
        assert(ctx.deployment.state_data);
312

313
        ctx.deployment.state_data->state = DatabaseStateString();
534✔
314

315
        log::Trace("Storing deployment state in the DB (database-string): " + DatabaseStateString());
1,068✔
316

317
        auto err = ctx.SaveDeploymentStateData(*ctx.deployment.state_data);
534✔
318
        if (err != error::NoError) {
534✔
319
                log::Error(err.String());
10✔
320
                if (err.code
10✔
321
                        == main_context::MakeError(main_context::StateDataStoreCountExceededError, "").code) {
10✔
322
                        poster.PostEvent(StateEvent::StateLoopDetected);
1✔
323
                        return;
324
                } else if (!IsFailureState()) {
9✔
325
                        // Non-failure states should be interrupted, but failure states should be
326
                        // allowed to do their work, even if a database error was detected.
327
                        poster.PostEvent(StateEvent::Failure);
2✔
328
                        return;
329
                }
330
        }
331

332
        OnEnterSaveState(ctx, poster);
531✔
333
}
334

335
void UpdateDownloadState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
53✔
336
        log::Debug("Entering Download state");
106✔
337

338
        auto req = make_shared<http::OutgoingRequest>();
53✔
339
        req->SetMethod(http::Method::GET);
53✔
340
        auto err = req->SetAddress(ctx.deployment.state_data->update_info.artifact.source.uri);
53✔
341
        if (err != error::NoError) {
53✔
342
                log::Error(err.String());
×
343
                poster.PostEvent(StateEvent::Failure);
×
344
                return;
345
        }
346

347
        err = ctx.download_client->AsyncCall(
53✔
348
                req,
349
                [&ctx, &poster](http::ExpectedIncomingResponsePtr exp_resp) {
105✔
350
                        if (!exp_resp) {
53✔
351
                                log::Error("Unexpected error during download: " + exp_resp.error().String());
×
352
                                poster.PostEvent(StateEvent::Failure);
×
353
                                return;
1✔
354
                        }
355

356
                        auto &resp = exp_resp.value();
53✔
357
                        if (resp->GetStatusCode() != http::StatusOK) {
53✔
358
                                log::Error(
1✔
359
                                        "Unexpected status code while fetching artifact: " + resp->GetStatusMessage());
2✔
360
                                poster.PostEvent(StateEvent::Failure);
1✔
361
                                return;
1✔
362
                        }
363

364
                        auto http_reader = resp->MakeBodyAsyncReader();
52✔
365
                        if (!http_reader) {
52✔
366
                                log::Error(http_reader.error().String());
×
367
                                poster.PostEvent(StateEvent::Failure);
×
368
                                return;
369
                        }
370
                        ctx.deployment.artifact_reader =
371
                                make_shared<events::io::ReaderFromAsyncReader>(ctx.event_loop, http_reader.value());
52✔
372
                        ParseArtifact(ctx, poster);
52✔
373
                },
374
                [](http::ExpectedIncomingResponsePtr exp_resp) {
53✔
375
                        if (!exp_resp) {
53✔
376
                                log::Error(exp_resp.error().String());
6✔
377
                                // Cannot handle error here, because this handler is called at the
378
                                // end of the download, when we have already left this state. So
379
                                // rely on this error being propagated through the BodyAsyncReader
380
                                // above instead.
381
                                return;
6✔
382
                        }
383
                });
106✔
384

385
        if (err != error::NoError) {
53✔
386
                log::Error(err.String());
×
387
                poster.PostEvent(StateEvent::Failure);
×
388
                return;
389
        }
390
}
391

392
void UpdateDownloadState::ParseArtifact(Context &ctx, sm::EventPoster<StateEvent> &poster) {
52✔
393
        string art_scripts_path = ctx.mender_context.GetConfig().paths.GetArtScriptsPath();
52✔
394

395
        // Clear the artifact scripts directory so we don't risk old scripts lingering.
396
        auto err = path::DeleteRecursively(art_scripts_path);
52✔
397
        if (err != error::NoError) {
52✔
398
                log::Error("When preparing to parse artifact: " + err.String());
×
399
                poster.PostEvent(StateEvent::Failure);
×
400
                return;
401
        }
402

403
        artifact::config::ParserConfig config {
52✔
404
                .artifact_scripts_filesystem_path = art_scripts_path,
405
                .artifact_scripts_version = 3,
406
                .artifact_verify_keys = ctx.mender_context.GetConfig().artifact_verify_keys,
52✔
407
        };
100✔
408
        auto exp_parser = artifact::Parse(*ctx.deployment.artifact_reader, config);
104✔
409
        if (!exp_parser) {
52✔
410
                log::Error(exp_parser.error().String());
×
411
                poster.PostEvent(StateEvent::Failure);
×
412
                return;
413
        }
414
        ctx.deployment.artifact_parser.reset(new artifact::Artifact(std::move(exp_parser.value())));
52✔
415

416
        auto exp_header = artifact::View(*ctx.deployment.artifact_parser, 0);
52✔
417
        if (!exp_header) {
52✔
418
                log::Error(exp_header.error().String());
×
419
                poster.PostEvent(StateEvent::Failure);
×
420
                return;
421
        }
422
        auto &header = exp_header.value();
52✔
423

424
        auto exp_matches = ctx.mender_context.MatchesArtifactDepends(header.header);
52✔
425
        if (!exp_matches) {
52✔
426
                log::Error(exp_matches.error().String());
2✔
427
                poster.PostEvent(StateEvent::Failure);
2✔
428
                return;
429
        } else if (!exp_matches.value()) {
50✔
430
                // reasons already logged
431
                poster.PostEvent(StateEvent::Failure);
1✔
432
                return;
433
        }
434

435
        log::Info("Installing artifact...");
98✔
436

437
        ctx.deployment.state_data->FillUpdateDataFromArtifact(header);
49✔
438

439
        ctx.deployment.state_data->state = Context::kUpdateStateDownload;
49✔
440

441
        assert(ctx.deployment.state_data->update_info.artifact.payload_types.size() == 1);
442

443
        // Initial state data save, now that we have enough information from the artifact.
444
        err = ctx.SaveDeploymentStateData(*ctx.deployment.state_data);
49✔
445
        if (err != error::NoError) {
49✔
446
                log::Error(err.String());
×
447
                if (err.code
×
448
                        == main_context::MakeError(main_context::StateDataStoreCountExceededError, "").code) {
×
449
                        poster.PostEvent(StateEvent::StateLoopDetected);
×
450
                        return;
451
                } else {
452
                        poster.PostEvent(StateEvent::Failure);
×
453
                        return;
454
                }
455
        }
456

457
        if (header.header.payload_type == "") {
49✔
458
                // Empty-payload-artifact, aka "bootstrap artifact".
459
                poster.PostEvent(StateEvent::NothingToDo);
1✔
460
                return;
461
        }
462

463
        ctx.deployment.update_module.reset(
464
                new update_module::UpdateModule(ctx.mender_context, header.header.payload_type));
48✔
465

466
        err = ctx.deployment.update_module->CleanAndPrepareFileTree(
48✔
467
                ctx.deployment.update_module->GetUpdateModuleWorkDir(), header);
48✔
468
        if (err != error::NoError) {
48✔
469
                log::Error(err.String());
×
470
                poster.PostEvent(StateEvent::Failure);
×
471
                return;
472
        }
473

474
        err = ctx.deployment.update_module->AsyncProvidePayloadFileSizes(
48✔
475
                ctx.event_loop, [&ctx, &poster](expected::ExpectedBool download_with_sizes) {
48✔
476
                        if (!download_with_sizes.has_value()) {
48✔
477
                                log::Error(download_with_sizes.error().String());
×
478
                                poster.PostEvent(StateEvent::Failure);
×
479
                                return;
×
480
                        }
481
                        ctx.deployment.download_with_sizes = download_with_sizes.value();
48✔
482
                        DoDownload(ctx, poster);
48✔
483
                });
48✔
484

485
        if (err != error::NoError) {
48✔
486
                log::Error(err.String());
×
487
                poster.PostEvent(StateEvent::Failure);
×
488
                return;
489
        }
490
}
491

492
void UpdateDownloadState::DoDownload(Context &ctx, sm::EventPoster<StateEvent> &poster) {
48✔
493
        auto exp_payload = ctx.deployment.artifact_parser->Next();
48✔
494
        if (!exp_payload) {
48✔
495
                log::Error(exp_payload.error().String());
×
496
                poster.PostEvent(StateEvent::Failure);
×
497
                return;
498
        }
499
        ctx.deployment.artifact_payload.reset(new artifact::Payload(std::move(exp_payload.value())));
48✔
500

501
        auto handler = [&poster, &ctx](error::Error err) {
46✔
502
                if (err != error::NoError) {
48✔
503
                        log::Error(err.String());
2✔
504
                        poster.PostEvent(StateEvent::Failure);
2✔
505
                        return;
2✔
506
                }
507

508
                auto exp_payload = ctx.deployment.artifact_parser->Next();
46✔
509
                if (exp_payload) {
46✔
510
                        log::Error("Multiple payloads are not yet supported in daemon mode.");
×
511
                        poster.PostEvent(StateEvent::Failure);
×
512
                        return;
513
                } else if (
46✔
514
                        exp_payload.error().code
515
                        != artifact::parser_error::MakeError(artifact::parser_error::EOFError, "").code) {
46✔
516
                        log::Error(exp_payload.error().String());
×
517
                        poster.PostEvent(StateEvent::Failure);
×
518
                        return;
519
                }
520

521
                poster.PostEvent(StateEvent::Success);
46✔
522
        };
523

524
        if (ctx.deployment.download_with_sizes) {
48✔
525
                ctx.deployment.update_module->AsyncDownloadWithFileSizes(
1✔
526
                        ctx.event_loop, *ctx.deployment.artifact_payload, handler);
1✔
527
        } else {
528
                ctx.deployment.update_module->AsyncDownload(
47✔
529
                        ctx.event_loop, *ctx.deployment.artifact_payload, handler);
47✔
530
        }
531
}
532

533
void UpdateDownloadCancelState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
6✔
534
        log::Debug("Entering DownloadCancel state");
12✔
535
        ctx.download_client->Cancel();
6✔
536
        poster.PostEvent(StateEvent::Success);
6✔
537
}
6✔
538

539
SendStatusUpdateState::SendStatusUpdateState(optional<deployments::DeploymentStatus> status) :
×
540
        status_(status),
541
        mode_(FailureMode::Ignore) {
×
542
}
×
543

544
SendStatusUpdateState::SendStatusUpdateState(
188✔
545
        optional<deployments::DeploymentStatus> status,
546
        events::EventLoop &event_loop,
547
        int retry_interval_seconds,
548
        int retry_count) :
549
        status_(status),
550
        mode_(FailureMode::RetryThenFail),
551
        retry_(Retry {
188✔
552
                http::ExponentialBackoff(chrono::seconds(retry_interval_seconds), retry_count),
553
                event_loop}) {
564✔
554
}
188✔
555

556
void SendStatusUpdateState::SetSmallestWaitInterval(chrono::milliseconds interval) {
178✔
557
        if (retry_) {
178✔
558
                retry_->backoff.SetSmallestInterval(interval);
178✔
559
        }
560
}
178✔
561

562
void SendStatusUpdateState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
242✔
563
        // Reset this every time we enter the state, which means a new round of retries.
564
        if (retry_) {
242✔
565
                retry_->backoff.Reset();
566
        }
567

568
        DoStatusUpdate(ctx, poster);
242✔
569
}
242✔
570

571
void SendStatusUpdateState::DoStatusUpdate(Context &ctx, sm::EventPoster<StateEvent> &poster) {
261✔
572
        assert(ctx.deployment_client);
573
        assert(ctx.deployment.state_data);
574

575
        log::Info("Sending status update to server");
522✔
576

577
        auto result_handler = [this, &ctx, &poster](const error::Error &err) {
566✔
578
                if (err != error::NoError) {
261✔
579
                        log::Error("Could not send deployment status: " + err.String());
48✔
580

581
                        switch (mode_) {
24✔
582
                        case FailureMode::Ignore:
583
                                break;
3✔
584
                        case FailureMode::RetryThenFail:
585
                                if (err.code
21✔
586
                                        == deployments::MakeError(deployments::DeploymentAbortedError, "").code) {
21✔
587
                                        // If the deployment was aborted upstream it is an immediate
588
                                        // failure, even if retry is enabled.
589
                                        poster.PostEvent(StateEvent::Failure);
1✔
590
                                        return;
21✔
591
                                }
592

593
                                auto exp_interval = retry_->backoff.NextInterval();
20✔
594
                                if (!exp_interval) {
20✔
595
                                        log::Error(
1✔
596
                                                "Giving up on sending status updates to server: "
597
                                                + exp_interval.error().String());
2✔
598
                                        poster.PostEvent(StateEvent::Failure);
1✔
599
                                        return;
600
                                }
601

602
                                log::Info(
19✔
603
                                        "Retrying status update after "
604
                                        + to_string(chrono::duration_cast<chrono::seconds>(*exp_interval).count())
38✔
605
                                        + " seconds");
38✔
606
                                retry_->wait_timer.AsyncWait(
19✔
607
                                        *exp_interval, [this, &ctx, &poster](error::Error err) {
38✔
608
                                                // Error here is quite unexpected (from a timer), so treat
609
                                                // this as an immediate error, despite Retry flag.
610
                                                if (err != error::NoError) {
19✔
611
                                                        log::Error(
×
612
                                                                "Unexpected error in SendStatusUpdateState wait timer: "
613
                                                                + err.String());
×
614
                                                        poster.PostEvent(StateEvent::Failure);
×
615
                                                        return;
×
616
                                                }
617

618
                                                // Try again. Since both status and logs are sent
619
                                                // from here, there's a chance this might resubmit
620
                                                // the status, but there's no harm in it, and it
621
                                                // won't happen often.
622
                                                DoStatusUpdate(ctx, poster);
19✔
623
                                        });
19✔
624
                                return;
19✔
625
                        }
626
                }
627

628
                poster.PostEvent(StateEvent::Success);
240✔
629
        };
261✔
630

631
        deployments::DeploymentStatus status;
632
        if (status_) {
261✔
633
                status = status_.value();
170✔
634
        } else {
635
                // If nothing is specified, grab success/failure status from the deployment status.
636
                if (ctx.deployment.failed) {
91✔
637
                        status = deployments::DeploymentStatus::Failure;
638
                } else {
639
                        status = deployments::DeploymentStatus::Success;
640
                }
641
        }
642

643
        // Push status.
644
        log::Debug("Pushing deployment status: " + DeploymentStatusString(status));
522✔
645
        auto err = ctx.deployment_client->PushStatus(
646
                ctx.deployment.state_data->update_info.id,
261✔
647
                status,
648
                "",
649
                ctx.http_client,
650
                [result_handler, &ctx](error::Error err) {
73✔
651
                        // If there is an error, we don't submit logs now, but call the handler,
652
                        // which may schedule a retry later. If there is no error, and the
653
                        // deployment as a whole was successful, then also call the handler here,
654
                        // since we don't need to submit logs at all then.
655
                        if (err != error::NoError || !ctx.deployment.failed) {
261✔
656
                                result_handler(err);
188✔
657
                                return;
188✔
658
                        }
659

660
                        // Push logs.
661
                        err = ctx.deployment_client->PushLogs(
73✔
662
                                ctx.deployment.state_data->update_info.id,
73✔
663
                                ctx.deployment.logger->LogFilePath(),
146✔
664
                                ctx.http_client,
665
                                result_handler);
73✔
666

667
                        if (err != error::NoError) {
73✔
668
                                result_handler(err);
×
669
                        }
670
                });
522✔
671

672
        if (err != error::NoError) {
261✔
673
                result_handler(err);
×
674
        }
675

676
        // No action, wait for reply from status endpoint.
677
}
261✔
678

679
void UpdateInstallState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
42✔
680
        log::Debug("Entering ArtifactInstall state");
84✔
681

682
        DefaultAsyncErrorHandler(
42✔
683
                poster,
684
                ctx.deployment.update_module->AsyncArtifactInstall(
42✔
685
                        ctx.event_loop, DefaultStateHandler {poster}));
42✔
686
}
42✔
687

688
void UpdateCheckRebootState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
73✔
689
        DefaultAsyncErrorHandler(
73✔
690
                poster,
691
                ctx.deployment.update_module->AsyncNeedsReboot(
73✔
692
                        ctx.event_loop, [&ctx, &poster](update_module::ExpectedRebootAction reboot_action) {
144✔
693
                                if (!reboot_action.has_value()) {
73✔
694
                                        log::Error(reboot_action.error().String());
2✔
695
                                        poster.PostEvent(StateEvent::Failure);
2✔
696
                                        return;
2✔
697
                                }
698

699
                                ctx.deployment.state_data->update_info.reboot_requested.resize(1);
71✔
700
                                ctx.deployment.state_data->update_info.reboot_requested[0] =
701
                                        NeedsRebootToDbString(*reboot_action);
71✔
702
                                switch (*reboot_action) {
71✔
703
                                case update_module::RebootAction::No:
8✔
704
                                        poster.PostEvent(StateEvent::NothingToDo);
8✔
705
                                        break;
8✔
706
                                case update_module::RebootAction::Yes:
63✔
707
                                case update_module::RebootAction::Automatic:
708
                                        poster.PostEvent(StateEvent::Success);
63✔
709
                                        break;
63✔
710
                                }
711
                        }));
73✔
712
}
73✔
713

714
void UpdateRebootState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
27✔
715
        log::Debug("Entering ArtifactReboot state");
54✔
716

717
        assert(ctx.deployment.state_data->update_info.reboot_requested.size() == 1);
718
        auto exp_reboot_mode =
719
                DbStringToNeedsReboot(ctx.deployment.state_data->update_info.reboot_requested[0]);
27✔
720
        // Should always be true because we check it at load time.
721
        assert(exp_reboot_mode);
722

723
        switch (exp_reboot_mode.value()) {
27✔
724
        case update_module::RebootAction::No:
×
725
                // Should not happen because then we don't enter this state.
726
                assert(false);
727
                poster.PostEvent(StateEvent::Failure);
×
728
                break;
729
        case update_module::RebootAction::Yes:
27✔
730
                DefaultAsyncErrorHandler(
27✔
731
                        poster,
732
                        ctx.deployment.update_module->AsyncArtifactReboot(
27✔
733
                                ctx.event_loop, DefaultStateHandler {poster}));
27✔
734
                break;
27✔
735
        case update_module::RebootAction::Automatic:
×
736
                DefaultAsyncErrorHandler(
×
737
                        poster,
738
                        ctx.deployment.update_module->AsyncSystemReboot(
×
739
                                ctx.event_loop, DefaultStateHandler {poster}));
×
740
                break;
×
741
        }
742
}
27✔
743

744
void UpdateVerifyRebootState::OnEnterSaveState(Context &ctx, sm::EventPoster<StateEvent> &poster) {
30✔
745
        log::Debug("Entering ArtifactVerifyReboot state");
60✔
746

747
        ctx.deployment.update_module->EnsureRootfsImageFileTree(
30✔
748
                ctx.deployment.update_module->GetUpdateModuleWorkDir());
60✔
749

750
        DefaultAsyncErrorHandler(
30✔
751
                poster,
752
                ctx.deployment.update_module->AsyncArtifactVerifyReboot(
30✔
753
                        ctx.event_loop, DefaultStateHandler {poster}));
30✔
754
}
30✔
755

756
void UpdateBeforeCommitState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
23✔
757
        // It's possible that the update we have done has changed our credentials. Therefore it's
758
        // important that we try to log in from scratch and do not use the token we already have.
759
        ctx.http_client.ExpireToken();
23✔
760

761
        poster.PostEvent(StateEvent::Success);
23✔
762
}
23✔
763

764
void UpdateCommitState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
19✔
765
        log::Debug("Entering ArtifactCommit state");
38✔
766

767
        // Explicitly check if state scripts version is supported
768
        auto err = script_executor::CheckScriptsCompatibility(
769
                ctx.mender_context.GetConfig().paths.GetRootfsScriptsPath());
19✔
770
        if (err != error::NoError) {
19✔
771
                log::Error("Failed script compatibility check: " + err.String());
×
772
                poster.PostEvent(StateEvent::Failure);
×
773
                return;
774
        }
775

776
        DefaultAsyncErrorHandler(
19✔
777
                poster,
778
                ctx.deployment.update_module->AsyncArtifactCommit(
19✔
779
                        ctx.event_loop, DefaultStateHandler {poster}));
38✔
780
}
781

782
void UpdateAfterCommitState::OnEnterSaveState(Context &ctx, sm::EventPoster<StateEvent> &poster) {
19✔
783
        // Now we have committed. If we had a schema update, re-save state data with the new schema.
784
        assert(ctx.deployment.state_data);
785
        auto &state_data = *ctx.deployment.state_data;
786
        if (state_data.update_info.has_db_schema_update) {
19✔
787
                state_data.update_info.has_db_schema_update = false;
×
788
                auto err = ctx.SaveDeploymentStateData(state_data);
×
789
                if (err != error::NoError) {
×
790
                        log::Error("Not able to commit schema update: " + err.String());
×
791
                        poster.PostEvent(StateEvent::Failure);
×
792
                        return;
793
                }
794
        }
795

796
        poster.PostEvent(StateEvent::Success);
19✔
797
}
798

799
void UpdateCheckRollbackState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
45✔
800
        DefaultAsyncErrorHandler(
45✔
801
                poster,
802
                ctx.deployment.update_module->AsyncSupportsRollback(
45✔
803
                        ctx.event_loop, [&ctx, &poster](expected::ExpectedBool rollback_supported) {
89✔
804
                                if (!rollback_supported.has_value()) {
45✔
805
                                        log::Error(rollback_supported.error().String());
1✔
806
                                        poster.PostEvent(StateEvent::Failure);
1✔
807
                                        return;
1✔
808
                                }
809

810
                                ctx.deployment.state_data->update_info.supports_rollback =
811
                                        SupportsRollbackToDbString(*rollback_supported);
44✔
812
                                if (*rollback_supported) {
44✔
813
                                        poster.PostEvent(StateEvent::RollbackStarted);
38✔
814
                                        poster.PostEvent(StateEvent::Success);
38✔
815
                                } else {
816
                                        poster.PostEvent(StateEvent::NothingToDo);
6✔
817
                                }
818
                        }));
45✔
819
}
45✔
820

821
void UpdateRollbackState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
41✔
822
        log::Debug("Entering ArtifactRollback state");
82✔
823

824
        DefaultAsyncErrorHandler(
41✔
825
                poster,
826
                ctx.deployment.update_module->AsyncArtifactRollback(
41✔
827
                        ctx.event_loop, DefaultStateHandler {poster}));
41✔
828
}
41✔
829

830
void UpdateRollbackRebootState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
57✔
831
        log::Debug("Entering ArtifactRollbackReboot state");
114✔
832

833
        auto exp_reboot_mode =
834
                DbStringToNeedsReboot(ctx.deployment.state_data->update_info.reboot_requested[0]);
57✔
835
        // Should always be true because we check it at load time.
836
        assert(exp_reboot_mode);
837

838
        // We ignore errors in this state as long as the ArtifactVerifyRollbackReboot state
839
        // succeeds.
840
        auto handler = [&poster](error::Error err) {
114✔
841
                if (err != error::NoError) {
57✔
842
                        log::Error(err.String());
2✔
843
                }
844
                poster.PostEvent(StateEvent::Success);
57✔
845
        };
57✔
846

847
        error::Error err;
57✔
848
        switch (exp_reboot_mode.value()) {
57✔
849
        case update_module::RebootAction::No:
850
                // Should not happen because then we don't enter this state.
851
                assert(false);
852

853
                err = error::MakeError(
×
854
                        error::ProgrammingError, "Entered UpdateRollbackRebootState with RebootAction = No");
×
855
                break;
×
856

857
        case update_module::RebootAction::Yes:
57✔
858
                err = ctx.deployment.update_module->AsyncArtifactRollbackReboot(ctx.event_loop, handler);
114✔
859
                break;
57✔
860

861
        case update_module::RebootAction::Automatic:
×
862
                err = ctx.deployment.update_module->AsyncSystemReboot(ctx.event_loop, handler);
×
863
                break;
×
864
        }
865

866
        if (err != error::NoError) {
57✔
867
                log::Error(err.String());
×
868
                poster.PostEvent(StateEvent::Success);
×
869
        }
870
}
57✔
871

872
void UpdateVerifyRollbackRebootState::OnEnterSaveState(
60✔
873
        Context &ctx, sm::EventPoster<StateEvent> &poster) {
874
        log::Debug("Entering ArtifactVerifyRollbackReboot state");
120✔
875

876
        // In this state we only retry, we don't fail. If this keeps on going forever, then the
877
        // state loop detection will eventually kick in.
878
        auto err = ctx.deployment.update_module->AsyncArtifactVerifyRollbackReboot(
879
                ctx.event_loop, [&poster](error::Error err) {
120✔
880
                        if (err != error::NoError) {
60✔
881
                                log::Error(err.String());
22✔
882
                                poster.PostEvent(StateEvent::Retry);
22✔
883
                                return;
22✔
884
                        }
885
                        poster.PostEvent(StateEvent::Success);
38✔
886
                });
60✔
887
        if (err != error::NoError) {
60✔
888
                log::Error(err.String());
×
889
                poster.PostEvent(StateEvent::Retry);
×
890
        }
891
}
60✔
892

893
void UpdateRollbackSuccessfulState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
50✔
894
        ctx.deployment.state_data->update_info.all_rollbacks_successful = true;
50✔
895
        poster.PostEvent(StateEvent::Success);
50✔
896
}
50✔
897

898
void UpdateFailureState::OnEnterSaveState(Context &ctx, sm::EventPoster<StateEvent> &poster) {
55✔
899
        log::Debug("Entering ArtifactFailure state");
110✔
900

901
        DefaultAsyncErrorHandler(
55✔
902
                poster,
903
                ctx.deployment.update_module->AsyncArtifactFailure(
55✔
904
                        ctx.event_loop, DefaultStateHandler {poster}));
55✔
905
}
55✔
906

907
static string AddInconsistentSuffix(const string &str) {
21✔
908
        const auto &suffix = main_context::MenderContext::broken_artifact_name_suffix;
909
        // `string::ends_with` is C++20... grumble
910
        string ret {str};
21✔
911
        if (!common::EndsWith(ret, suffix)) {
21✔
912
                ret.append(suffix);
21✔
913
        }
914
        return ret;
21✔
915
}
916

917
void UpdateSaveProvidesState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
75✔
918
        if (ctx.deployment.failed && !ctx.deployment.rollback_failed) {
75✔
919
                // If the update failed, but we rolled back successfully, then we don't need to do
920
                // anything, just keep the old data.
921
                poster.PostEvent(StateEvent::Success);
38✔
922
                return;
38✔
923
        }
924

925
        assert(ctx.deployment.state_data);
926
        // This state should never happen: rollback failed, but update not failed??
927
        assert(!(!ctx.deployment.failed && ctx.deployment.rollback_failed));
928

929
        // We expect Cleanup to be the next state after this.
930
        ctx.deployment.state_data->state = ctx.kUpdateStateCleanup;
37✔
931

932
        auto &artifact = ctx.deployment.state_data->update_info.artifact;
933

934
        string artifact_name;
935
        if (ctx.deployment.rollback_failed) {
37✔
936
                artifact_name = AddInconsistentSuffix(artifact.artifact_name);
38✔
937
        } else {
938
                artifact_name = artifact.artifact_name;
18✔
939
        }
940

941
        bool deploy_failed = ctx.deployment.failed;
37✔
942

943
        // Only the artifact_name and group should be committed in the case of a
944
        // failing update in order to make this consistent with the old client
945
        // behaviour.
946
        auto err = ctx.mender_context.CommitArtifactData(
37✔
947
                artifact_name,
948
                artifact.artifact_group,
37✔
949
                deploy_failed ? nullopt : optional<context::ProvidesData>(artifact.type_info_provides),
74✔
950
                /* Special case: Keep existing provides */
951
                deploy_failed ? context::ClearsProvidesData {}
93✔
952
                                          : optional<context::ClearsProvidesData>(artifact.clears_artifact_provides),
18✔
953
                [&ctx](kv_db::Transaction &txn) {
37✔
954
                        // Save the Cleanup state together with the artifact data, atomically.
955
                        return ctx.SaveDeploymentStateData(txn, *ctx.deployment.state_data);
37✔
956
                });
74✔
957
        if (err != error::NoError) {
37✔
958
                log::Error("Error saving artifact data: " + err.String());
×
959
                if (err.code
×
960
                        == main_context::MakeError(main_context::StateDataStoreCountExceededError, "").code) {
×
961
                        poster.PostEvent(StateEvent::StateLoopDetected);
×
962
                        return;
963
                }
964
                poster.PostEvent(StateEvent::Failure);
×
965
                return;
966
        }
967

968
        poster.PostEvent(StateEvent::Success);
37✔
969
}
970

971
void UpdateCleanupState::OnEnterSaveState(Context &ctx, sm::EventPoster<StateEvent> &poster) {
89✔
972
        log::Debug("Entering ArtifactCleanup state");
178✔
973

974
        // It's possible for there not to be an initialized update_module structure, if the
975
        // deployment failed before we could successfully parse the artifact. If so, cleanup is a
976
        // no-op.
977
        if (!ctx.deployment.update_module) {
89✔
978
                poster.PostEvent(StateEvent::Success);
8✔
979
                return;
8✔
980
        }
981

982
        DefaultAsyncErrorHandler(
81✔
983
                poster,
984
                ctx.deployment.update_module->AsyncCleanup(ctx.event_loop, DefaultStateHandler {poster}));
162✔
985
}
986

987
void ClearArtifactDataState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
91✔
988
        auto err = ctx.mender_context.GetMenderStoreDB().WriteTransaction([](kv_db::Transaction &txn) {
91✔
989
                // Remove state data, since we're done now.
990
                auto err = txn.Remove(main_context::MenderContext::state_data_key);
89✔
991
                if (err != error::NoError) {
89✔
992
                        return err;
×
993
                }
994
                return txn.Remove(main_context::MenderContext::state_data_key_uncommitted);
89✔
995
        });
91✔
996
        if (err != error::NoError) {
91✔
997
                log::Error("Error removing artifact data: " + err.String());
4✔
998
                poster.PostEvent(StateEvent::Failure);
2✔
999
                return;
1000
        }
1001

1002
        poster.PostEvent(StateEvent::Success);
89✔
1003
}
1004

1005
void StateLoopState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
2✔
1006
        assert(ctx.deployment.state_data);
1007
        auto &artifact = ctx.deployment.state_data->update_info.artifact;
1008

1009
        // Mark update as inconsistent.
1010
        string artifact_name = AddInconsistentSuffix(artifact.artifact_name);
2✔
1011

1012
        auto err = ctx.mender_context.CommitArtifactData(
2✔
1013
                artifact_name,
1014
                artifact.artifact_group,
2✔
1015
                artifact.type_info_provides,
2✔
1016
                artifact.clears_artifact_provides,
2✔
1017
                [](kv_db::Transaction &txn) { return error::NoError; });
6✔
1018
        if (err != error::NoError) {
2✔
1019
                log::Error("Error saving inconsistent artifact data: " + err.String());
×
1020
                poster.PostEvent(StateEvent::Failure);
×
1021
                return;
1022
        }
1023

1024
        poster.PostEvent(StateEvent::Success);
2✔
1025
}
1026

1027
void EndOfDeploymentState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
91✔
1028
        log::Info(
91✔
1029
                "Deployment with ID " + ctx.deployment.state_data->update_info.id
182✔
1030
                + " finished with status: " + string(ctx.deployment.failed ? "Failure" : "Success"));
382✔
1031

1032
        ctx.FinishDeploymentLogging();
91✔
1033

1034
        ctx.deployment = {};
91✔
1035
        poster.PostEvent(
91✔
1036
                StateEvent::InventoryPollingTriggered); // Submit the inventory right after an update
91✔
1037
        poster.PostEvent(StateEvent::DeploymentEnded);
91✔
1038
        poster.PostEvent(StateEvent::Success);
91✔
1039
}
91✔
1040

1041
ExitState::ExitState(events::EventLoop &event_loop) :
94✔
1042
        event_loop_(event_loop) {
188✔
1043
}
94✔
1044

1045
void ExitState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
91✔
1046
#ifndef NDEBUG
1047
        if (--iterations_left_ <= 0) {
1048
                event_loop_.Stop();
1049
        } else {
1050
                poster.PostEvent(StateEvent::Success);
1051
        }
1052
#else
1053
        event_loop_.Stop();
91✔
1054
#endif
1055
}
91✔
1056

1057
namespace deployment_tracking {
1058

1059
void NoFailuresState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
61✔
1060
        ctx.deployment.failed = false;
61✔
1061
        ctx.deployment.rollback_failed = false;
61✔
1062
}
61✔
1063

1064
void FailureState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
58✔
1065
        ctx.deployment.failed = true;
58✔
1066
        ctx.deployment.rollback_failed = true;
58✔
1067
}
58✔
1068

1069
void RollbackAttemptedState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
52✔
1070
        ctx.deployment.failed = true;
52✔
1071
        ctx.deployment.rollback_failed = false;
52✔
1072
}
52✔
1073

1074
void RollbackFailedState::OnEnter(Context &ctx, sm::EventPoster<StateEvent> &poster) {
12✔
1075
        ctx.deployment.failed = true;
12✔
1076
        ctx.deployment.rollback_failed = true;
12✔
1077
}
12✔
1078

1079
} // namespace deployment_tracking
1080

1081
} // namespace daemon
1082
} // namespace update
1083
} // 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