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

mendersoftware / mender / 2434557270

07 Apr 2026 01:18PM UTC coverage: 81.686% (-0.01%) from 81.7%
2434557270

push

gitlab-ci

web-flow
Merge pull request #1931 from vpodzime/5.1.x-renovate_mender-artifact

[5.1.x] ci: Add renovate CI job for updating mender-artifact version

9059 of 11090 relevant lines covered (81.69%)

20531.32 hits per line

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

81.71
/src/mender-update/deployments/deployments.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/deployments.hpp>
16

17
#include <algorithm>
18
#include <sstream>
19
#include <string>
20

21
#include <api/api.hpp>
22
#include <api/client.hpp>
23
#include <common/common.hpp>
24
#include <common/error.hpp>
25
#include <common/events.hpp>
26
#include <common/expected.hpp>
27
#include <common/http.hpp>
28
#include <common/io.hpp>
29
#include <common/json.hpp>
30
#include <common/log.hpp>
31
#include <common/optional.hpp>
32
#include <common/path.hpp>
33
#include <mender-update/context.hpp>
34

35
namespace mender {
36
namespace update {
37
namespace deployments {
38

39
using namespace std;
40

41
namespace api = mender::api;
42
namespace common = mender::common;
43
namespace context = mender::update::context;
44
namespace error = mender::common::error;
45
namespace events = mender::common::events;
46
namespace expected = mender::common::expected;
47
namespace http = mender::common::http;
48
namespace io = mender::common::io;
49
namespace json = mender::common::json;
50
namespace log = mender::common::log;
51
namespace path = mender::common::path;
52

53
using ExpectedOffset = expected::expected<ifstream::off_type, error::Error>;
54

55
const DeploymentsErrorCategoryClass DeploymentsErrorCategory;
56

57
const char *DeploymentsErrorCategoryClass::name() const noexcept {
×
58
        return "DeploymentsErrorCategory";
×
59
}
60

61
string DeploymentsErrorCategoryClass::message(int code) const {
33✔
62
        switch (code) {
33✔
63
        case NoError:
64
                return "Success";
×
65
        case InvalidDataError:
66
                return "Invalid data error";
×
67
        case BadResponseError:
68
                return "Bad response error";
4✔
69
        case DeploymentAbortedError:
70
                return "Deployment was aborted on the server";
3✔
71
        case TooManyRequestsError:
72
                return "Too many requests";
26✔
73
        case RequestBodyTooLargeError:
74
                return "Request body too large";
×
75
        }
76
        assert(false);
77
        return "Unknown";
×
78
}
79

80
error::Error MakeError(DeploymentsErrorCode code, const string &msg) {
89✔
81
        return error::Error(error_condition(code, DeploymentsErrorCategory), msg);
104✔
82
}
83

84
static const string check_updates_v1_uri = "/api/devices/v1/deployments/device/deployments/next";
85
static const string check_updates_v2_uri = "/api/devices/v2/deployments/device/deployments/next";
86

87
error::Error DeploymentClient::CheckNewDeployments(
10✔
88
        context::MenderContext &ctx, api::Client &client, CheckUpdatesAPIResponseHandler api_handler) {
89
        auto ex_compatible_type = ctx.GetCompatibleType();
20✔
90
        if (!ex_compatible_type) {
10✔
91
                return ex_compatible_type.error();
4✔
92
        }
93
        string compatible_type = ex_compatible_type.value();
6✔
94

95
        auto ex_provides = ctx.LoadProvides();
6✔
96
        if (!ex_provides) {
6✔
97
                return ex_provides.error();
×
98
        }
99
        auto provides = ex_provides.value();
6✔
100
        if (provides.find("artifact_name") == provides.end()) {
12✔
101
                return MakeError(InvalidDataError, "Missing artifact name data");
×
102
        }
103

104
        stringstream ss;
6✔
105
        ss << R"({"device_provides":{)";
6✔
106
        ss << R"("device_type":")";
6✔
107
        ss << json::EscapeString(compatible_type);
12✔
108

109
        for (const auto &kv : provides) {
14✔
110
                ss << "\",\"" + json::EscapeString(kv.first) + "\":\"";
8✔
111
                ss << json::EscapeString(kv.second);
16✔
112
        }
113

114
        ss << R"("}})";
6✔
115

116
        string v2_payload = ss.str();
117
        log::Debug("deployments/next v2 payload " + v2_payload);
6✔
118
        http::BodyGenerator payload_gen = [v2_payload]() {
54✔
119
                return make_shared<io::StringReader>(v2_payload);
6✔
120
        };
6✔
121

122
        auto v2_req = make_shared<api::APIRequest>();
6✔
123
        v2_req->SetPath(check_updates_v2_uri);
124
        v2_req->SetMethod(http::Method::POST);
6✔
125
        v2_req->SetHeader("Content-Type", "application/json");
12✔
126
        v2_req->SetHeader("Content-Length", to_string(v2_payload.size()));
12✔
127
        v2_req->SetHeader("Accept", "application/json");
12✔
128
        v2_req->SetBodyGenerator(payload_gen);
6✔
129

130
        string v1_args = "artifact_name=" + http::URLEncode(provides["artifact_name"])
12✔
131
                                         + "&device_type=" + http::URLEncode(compatible_type);
18✔
132
        auto v1_req = make_shared<api::APIRequest>();
6✔
133
        v1_req->SetPath(check_updates_v1_uri + "?" + v1_args);
6✔
134
        v1_req->SetMethod(http::Method::GET);
6✔
135
        v1_req->SetHeader("Accept", "application/json");
12✔
136

137
        auto received_body = make_shared<vector<uint8_t>>();
6✔
138
        auto handle_data = [received_body, api_handler](unsigned status) {
4✔
139
                if (status == http::StatusOK) {
4✔
140
                        auto ex_j = json::Load(common::StringFromByteVector(*received_body));
4✔
141
                        if (ex_j) {
2✔
142
                                CheckUpdatesAPIResponse response {optional<json::Json> {ex_j.value()}};
2✔
143
                                api_handler(response);
4✔
144
                        } else {
145
                                api_handler(expected::unexpected(
×
146
                                        CheckUpdatesAPIResponseError {status, nullopt, ex_j.error()}));
×
147
                        }
148
                } else if (status == http::StatusNoContent) {
2✔
149
                        api_handler(CheckUpdatesAPIResponse {nullopt});
4✔
150
                } else {
151
                        log::Warning(
×
152
                                "DeploymentClient::CheckNewDeployments - received unhandled http response: "
153
                                + to_string(status));
×
154
                        api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
×
155
                                status,
156
                                nullopt,
157
                                MakeError(
158
                                        DeploymentAbortedError,
159
                                        "received unhandled HTTP response: " + to_string(status))}));
×
160
                }
161
        };
10✔
162

163
        http::ResponseHandler header_handler =
164
                [this, received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
12✔
165
                        this->HeaderHandler(received_body, api_handler, exp_resp);
27✔
166
                };
15✔
167

168
        http::ResponseHandler v1_body_handler =
169
                [received_body, api_handler, handle_data](http::ExpectedIncomingResponsePtr exp_resp) {
15✔
170
                        if (!exp_resp) {
3✔
171
                                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
172
                                CheckUpdatesAPIResponse response = expected::unexpected(
×
173
                                        CheckUpdatesAPIResponseError {nullopt, nullopt, exp_resp.error()});
×
174
                                api_handler(response);
×
175
                                return;
176
                        }
177
                        auto resp = exp_resp.value();
3✔
178
                        auto status = resp->GetStatusCode();
3✔
179

180
                        // StatusTooManyRequests must have been handled in HeaderHandler already
181
                        assert(status != http::StatusTooManyRequests);
182

183
                        if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
3✔
184
                                handle_data(status);
2✔
185
                        } else {
186
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
187
                                string err_str;
188
                                if (ex_err_msg) {
1✔
189
                                        err_str = ex_err_msg.value();
×
190
                                } else {
191
                                        err_str = resp->GetStatusMessage();
2✔
192
                                }
193
                                api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
3✔
194
                                        status,
195
                                        nullopt,
196
                                        MakeError(
197
                                                BadResponseError,
198
                                                "Got unexpected response " + to_string(status) + ": " + err_str)}));
2✔
199
                        }
200
                };
6✔
201

202
        http::ResponseHandler v2_body_handler = [received_body,
18✔
203
                                                                                         v1_req,
204
                                                                                         header_handler,
205
                                                                                         v1_body_handler,
206
                                                                                         api_handler,
207
                                                                                         handle_data,
208
                                                                                         &client](http::ExpectedIncomingResponsePtr exp_resp) {
209
                if (!exp_resp) {
6✔
210
                        log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
211
                        CheckUpdatesAPIResponse response = expected::unexpected(
×
212
                                CheckUpdatesAPIResponseError {nullopt, nullopt, exp_resp.error()});
×
213
                        api_handler(response);
×
214
                        return;
215
                }
216
                auto resp = exp_resp.value();
6✔
217
                auto status = resp->GetStatusCode();
6✔
218

219
                // StatusTooManyRequests must have been handled in HeaderHandler already
220
                assert(status != http::StatusTooManyRequests);
221

222
                if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
6✔
223
                        handle_data(status);
2✔
224
                } else if (status == http::StatusNotFound) {
4✔
225
                        log::Debug(
3✔
226
                                "POST request to v2 version of the deployments API failed, falling back to v1 version and GET");
227
                        auto err = client.AsyncCall(v1_req, header_handler, v1_body_handler);
9✔
228
                        if (err != error::NoError) {
3✔
229
                                api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
×
230
                                        status, nullopt, err.WithContext("While calling v1 endpoint")}));
×
231
                        }
232
                } else {
233
                        auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
234
                        string err_str;
235
                        if (ex_err_msg) {
1✔
236
                                err_str = ex_err_msg.value();
1✔
237
                        } else {
238
                                err_str = resp->GetStatusMessage();
×
239
                        }
240
                        api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
3✔
241
                                status,
242
                                nullopt,
243
                                MakeError(
244
                                        BadResponseError,
245
                                        "Got unexpected response " + to_string(status) + ": " + err_str)}));
2✔
246
                }
247
        };
6✔
248

249
        return client.AsyncCall(v2_req, header_handler, v2_body_handler);
18✔
250
}
12✔
251

252
void DeploymentClient::HeaderHandler(
12✔
253
        shared_ptr<vector<uint8_t>> received_body,
254
        CheckUpdatesAPIResponseHandler api_handler,
255
        http::ExpectedIncomingResponsePtr exp_resp) {
256
        if (!exp_resp) {
12✔
257
                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
258
                CheckUpdatesAPIResponse response =
259
                        expected::unexpected(CheckUpdatesAPIResponseError {nullopt, nullopt, exp_resp.error()});
×
260
                api_handler(response);
×
261
                return;
262
        }
263

264
        auto resp = exp_resp.value();
12✔
265
        auto status = resp->GetStatusCode();
12✔
266
        if (status == http::StatusTooManyRequests) {
12✔
267
                CheckUpdatesAPIResponse response = expected::unexpected(CheckUpdatesAPIResponseError {
6✔
268
                        status, resp->GetHeaders(), MakeError(TooManyRequestsError, "Too many requests")});
9✔
269
                api_handler(response);
6✔
270
        }
271
        received_body->clear();
12✔
272
        auto body_writer = make_shared<io::ByteWriter>(received_body);
12✔
273
        body_writer->SetUnlimited(true);
12✔
274
        resp->SetBodyWriter(body_writer);
24✔
275
}
276

277
static const string deployment_status_strings[static_cast<int>(DeploymentStatus::End_) + 1] = {
278
        "installing",
279
        "pause_before_installing",
280
        "downloading",
281
        "pause_before_rebooting",
282
        "rebooting",
283
        "pause_before_committing",
284
        "success",
285
        "failure",
286
        "already-installed"};
287

288
static const string deployments_uri_prefix = "/api/devices/v1/deployments/device/deployments";
289
static const string status_uri_suffix = "/status";
290

291
string DeploymentStatusString(DeploymentStatus status) {
501✔
292
        return deployment_status_strings[static_cast<int>(status)];
505✔
293
}
294

295
error::Error DeploymentClient::PushStatus(
4✔
296
        const string &deployment_id,
297
        DeploymentStatus status,
298
        const string &substate,
299
        api::Client &client,
300
        StatusAPIResponseHandler api_handler) {
301
        // Cannot push a status update without a deployment ID
302
        AssertOrReturnError(deployment_id != "");
4✔
303
        string payload = R"({"status":")" + DeploymentStatusString(status) + "\"";
4✔
304
        if (substate != "") {
4✔
305
                payload += R"(,"substate":")" + json::EscapeString(substate) + "\"}";
6✔
306
        } else {
307
                payload += "}";
1✔
308
        }
309
        http::BodyGenerator payload_gen = [payload]() {
36✔
310
                return make_shared<io::StringReader>(payload);
4✔
311
        };
4✔
312

313
        auto req = make_shared<api::APIRequest>();
4✔
314
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, status_uri_suffix));
4✔
315
        req->SetMethod(http::Method::PUT);
4✔
316
        req->SetHeader("Content-Type", "application/json");
8✔
317
        req->SetHeader("Content-Length", to_string(payload.size()));
8✔
318
        req->SetHeader("Accept", "application/json");
8✔
319
        req->SetBodyGenerator(payload_gen);
4✔
320

321
        auto received_body = make_shared<vector<uint8_t>>();
4✔
322
        return client.AsyncCall(
16✔
323
                req,
324
                [this, received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
8✔
325
                        this->PushStatusHeaderHandler(received_body, api_handler, exp_resp);
12✔
326
                },
4✔
327
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
12✔
328
                        if (!exp_resp) {
4✔
329
                                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
330
                                api_handler(StatusAPIResponse {nullopt, nullopt, exp_resp.error()});
×
331
                                return;
×
332
                        }
333

334
                        auto resp = exp_resp.value();
4✔
335
                        auto status = resp->GetStatusCode();
4✔
336

337
                        // StatusTooManyRequests must have been handled in PushStatusHeaderHandler already
338
                        assert(status != http::StatusTooManyRequests);
339

340
                        if (status == http::StatusNoContent) {
4✔
341
                                api_handler(StatusAPIResponse {status, nullopt, error::NoError});
2✔
342
                        } else if (status == http::StatusConflict) {
2✔
343
                                api_handler(StatusAPIResponse {
2✔
344
                                        status,
345
                                        nullopt,
346
                                        MakeError(DeploymentAbortedError, "Could not send status update to server")});
2✔
347
                        } else {
348
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
349
                                string err_str;
350
                                if (ex_err_msg) {
1✔
351
                                        err_str = ex_err_msg.value();
1✔
352
                                } else {
353
                                        err_str = resp->GetStatusMessage();
×
354
                                }
355
                                api_handler(StatusAPIResponse {
2✔
356
                                        status,
357
                                        nullopt,
358
                                        MakeError(
359
                                                BadResponseError,
360
                                                "Got unexpected response " + to_string(status)
1✔
361
                                                        + " from status API: " + err_str)});
2✔
362
                        }
363
                });
4✔
364
}
365

366
void DeploymentClient::PushStatusHeaderHandler(
7✔
367
        shared_ptr<vector<uint8_t>> received_body,
368
        StatusAPIResponseHandler api_handler,
369
        http::ExpectedIncomingResponsePtr exp_resp) {
370
        if (!exp_resp) {
7✔
371
                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
372
                api_handler(StatusAPIResponse {nullopt, nullopt, exp_resp.error()});
×
373
                return;
×
374
        }
375

376
        auto body_writer = make_shared<io::ByteWriter>(received_body);
7✔
377
        auto resp = exp_resp.value();
7✔
378
        auto status = resp->GetStatusCode();
7✔
379
        if (status == http::StatusTooManyRequests) {
7✔
380
                StatusAPIResponse response = {
381
                        status, resp->GetHeaders(), MakeError(TooManyRequestsError, "Too many requests")};
3✔
382
                api_handler(response);
3✔
383
        }
3✔
384
        auto content_length = resp->GetHeader("Content-Length");
14✔
385
        if (!content_length) {
7✔
386
                log::Debug(
3✔
387
                        "Failed to get content length from the deployment status API response headers: "
388
                        + content_length.error().String());
6✔
389
                body_writer->SetUnlimited(true);
3✔
390
        } else {
391
                auto ex_len = common::StringTo<size_t>(content_length.value());
4✔
392
                if (!ex_len) {
4✔
393
                        log::Error(
×
394
                                "Failed to convert the content length from the deployment status API response headers to an integer: "
395
                                + ex_len.error().String());
×
396
                        body_writer->SetUnlimited(true);
×
397
                } else {
398
                        received_body->resize(ex_len.value());
4✔
399
                }
400
        }
401
        resp->SetBodyWriter(body_writer);
14✔
402
}
403

404
using mender::common::expected::ExpectedSize;
405

406
static ExpectedSize GetLogFileDataSize(const string &path) {
23✔
407
        auto ex_istr = io::OpenIfstream(path);
23✔
408
        if (!ex_istr) {
23✔
409
                return expected::unexpected(ex_istr.error());
×
410
        }
411
        auto istr = std::move(ex_istr.value());
23✔
412

413
        // We want the size of the actual data without a potential trailing
414
        // comma. So let's seek one byte before the end of file, check if the last
415
        // byte is a comma and return the appropriate number.
416
        istr.seekg(-1, ios_base::end);
23✔
417
        int c = istr.get();
23✔
418
        if (c == ',') {
23✔
419
                return istr.tellg() - static_cast<ifstream::off_type>(1);
23✔
420
        } else {
421
                return istr.tellg();
×
422
        }
423
}
23✔
424

425
// Find the beginning of the next log message JSON after offset
426
static ExpectedOffset FindNextMsgAfter(const string &path, ifstream::off_type offset) {
1✔
427
        auto ex_is = io::OpenIfstream(path);
1✔
428
        if (!ex_is) {
1✔
429
                return expected::unexpected(ex_is.error());
×
430
        }
431
        auto is = std::move(ex_is.value());
1✔
432
        is.seekg(offset);
1✔
433
        int io_errno = errno;
1✔
434
        if (!is) {
1✔
435
                return expected::unexpected(error::Error(
×
436
                        generic_category().default_error_condition(io_errno),
×
437
                        "Failed to seek to truncated logs offset in '" + path + "'"));
×
438
        }
439

440
        // Now that we have seeked to the starting offset, we need to find the next
441
        // boundary between JSON log entries.
442
        const string pattern = R"(},{"timestamp")";
1✔
443
        string buf(1024, '\0');
444
        while (is.read(buf.data(), 1024)) {
1✔
445
                // make sure we don't work with some stale data from the previous read()
446
                buf.resize(is.gcount());
1✔
447

448
                // XXX: This is not perfect as it can fail to find the next boundary if
449
                //      it's split between the end of the current buffer contents and
450
                //      the start of the next chunk read from the file. However, with a
451
                //      1K buffer, this is very unlikely to happen and even if it does
452
                //      happen, the logs will just be trimmed a bit more than
453
                //      necessary. For the sake of incomparably simpler code that is
454
                //      easy to test.
455
                auto pos = buf.find(pattern);
1✔
456
                if (pos != string::npos) {
1✔
457
                        offset += pos + 2; // skip "},"
1✔
458
                        break;
1✔
459
                }
460
                offset += buf.size();
×
461
        }
462
        io_errno = errno;
1✔
463
        if (!is && !is.eof()) {
1✔
464
                return expected::unexpected(error::Error(
×
465
                        generic_category().default_error_condition(io_errno),
×
466
                        "Failed to read logs from '" + path + "'"));
×
467
        }
468

469
        // In case we read the whole rest of the file not finding the next JSON log
470
        // entry, there must be something really wrong with the log and it should be
471
        // fully skipped. The user will still get the information that the log was
472
        // truncated and they will still have the full log on the device.
473
        return offset;
474
}
1✔
475

476
const vector<uint8_t> JsonLogMessagesReader::header_ = {
477
        '{', '"', 'm', 'e', 's', 's', 'a', 'g', 'e', 's', '"', ':', '['};
478
const vector<uint8_t> JsonLogMessagesReader::closing_ = {']', '}'};
479
const string JsonLogMessagesReader::default_tstamp_ = "1970-01-01T00:00:00.000000000Z";
480
const string JsonLogMessagesReader::bad_data_msg_tmpl_ =
481
        R"d({"timestamp": "1970-01-01T00:00:00.000000000Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},)d";
482
const string JsonLogMessagesReader::too_much_data_msg_tmpl_ =
483
        R"d({"timestamp": "1970-01-01T00:00:00.000000000Z", "level": "WARNING", "message": "(THE ORIGINAL LOGS WERE TOO BIG, THIS LOG IS TRUNCATED. The full log can be found on the device)"},)d";
484

485
JsonLogMessagesReader::~JsonLogMessagesReader() {
54✔
486
        reader_.reset();
487
        if (!sanitized_fpath_.empty() && path::FileExists(sanitized_fpath_)) {
18✔
488
                auto del_err = path::FileDelete(sanitized_fpath_);
18✔
489
                if (del_err != error::NoError) {
18✔
490
                        log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
491
                }
492
        }
493
        sanitized_fpath_.erase();
18✔
494
}
36✔
495

496
static error::Error DoSanitizeLogs(
23✔
497
        const string &orig_path, const string &new_path, bool &all_valid, string &first_tstamp) {
498
        auto ex_ifs = io::OpenIfstream(orig_path);
23✔
499
        if (!ex_ifs) {
23✔
500
                return ex_ifs.error();
×
501
        }
502
        auto ex_ofs = io::OpenOfstream(new_path);
23✔
503
        if (!ex_ofs) {
23✔
504
                return ex_ofs.error();
×
505
        }
506
        auto &ifs = ex_ifs.value();
23✔
507
        auto &ofs = ex_ofs.value();
23✔
508

509
        string last_known_tstamp = first_tstamp;
23✔
510
        const string tstamp_prefix_data = R"d({"timestamp": ")d";
23✔
511
        const string corrupt_msg_suffix_data =
512
                R"d(", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},)d";
23✔
513

514
        string line;
515
        first_tstamp.erase();
23✔
516
        all_valid = true;
23✔
517
        error::Error err;
23✔
518
        while (!ifs.eof()) {
20,109✔
519
                getline(ifs, line);
20,086✔
520
                if (!ifs.eof() && !ifs) {
20,086✔
521
                        int io_errno = errno;
×
522
                        return error::Error(
523
                                generic_category().default_error_condition(io_errno),
×
524
                                "Failed to get line from deployment logs file '" + orig_path
×
525
                                        + "': " + strerror(io_errno));
×
526
                }
527
                if (line.empty()) {
20,086✔
528
                        // skip empty lines
529
                        continue;
23✔
530
                }
531
                auto ex_json = json::Load(line);
40,126✔
532
                if (ex_json) {
20,063✔
533
                        // valid JSON log line, just replace the newline after it with a comma and save the
534
                        // timestamp for later
535
                        auto ex_tstamp = ex_json.value().Get("timestamp").and_then(json::ToString);
40,100✔
536
                        if (ex_tstamp) {
20,050✔
537
                                if (first_tstamp.empty()) {
20,050✔
538
                                        first_tstamp = ex_tstamp.value();
22✔
539
                                }
540
                                last_known_tstamp = std::move(ex_tstamp.value());
20,050✔
541
                        }
542
                        line.append(1, ',');
20,050✔
543
                        err = io::WriteStringIntoOfstream(ofs, line);
20,050✔
544
                        if (err != error::NoError) {
20,050✔
545
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
546
                        }
547
                } else {
548
                        all_valid = false;
13✔
549
                        if (first_tstamp.empty()) {
13✔
550
                                // If we still don't have the first valid tstamp, we need to
551
                                // save the last known one (potentially pre-set) as the first
552
                                // one.
553
                                first_tstamp = last_known_tstamp;
554
                        }
555
                        err = io::WriteStringIntoOfstream(
13✔
556
                                ofs, tstamp_prefix_data + last_known_tstamp + corrupt_msg_suffix_data);
26✔
557
                        if (err != error::NoError) {
13✔
558
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
559
                        }
560
                }
561
        }
562
        return error::NoError;
23✔
563
}
564

565
static void ReplaceTimestampInMsgData(
10✔
566
        vector<uint8_t> &msg_data, string &timestamp, size_t default_tstamp_size) {
567
        auto msg_data_tstamp_start = msg_data.begin() + 15; // len(R"({"timestamp": ")")
568

569
        // The actual timestamp from logs can potentially have a different
570
        // (likely lower) time resolution and thus length than our default.
571
        const auto timestamp_size = timestamp.size();
572
        if (timestamp_size > default_tstamp_size) {
10✔
573
                // In case the time resolution is higher and the timestamp
574
                // longer (unlikely to happen)
575
                if (timestamp[timestamp_size - 1] == 'Z') {
1✔
576
                        timestamp[default_tstamp_size - 1] = 'Z';
1✔
577
                }
578
                timestamp.resize(default_tstamp_size);
1✔
579
        }
580
        copy_n(timestamp.cbegin(), timestamp.size(), msg_data_tstamp_start);
10✔
581
        if (timestamp.size() < default_tstamp_size) {
10✔
582
                // Add a closing '"' right after the timestamp and fill in the
583
                // rest of the space in the template with spaces that have no
584
                // effect in JSON.
585
                msg_data_tstamp_start[timestamp.size()] = '"';
1✔
586
                for (auto it = msg_data_tstamp_start + timestamp.size() + 1;
1✔
587
                         it < msg_data_tstamp_start + default_tstamp_size + 1;
4✔
588
                         it++) {
589
                        *it = ' ';
3✔
590
                }
591
        }
592
}
10✔
593

594
error::Error JsonLogMessagesReader::SanitizeLogs() {
23✔
595
        if (!sanitized_fpath_.empty()) {
23✔
596
                return error::NoError;
×
597
        }
598

599
        string prep_fpath = log_fpath_ + ".sanitized";
23✔
600
        string first_tstamp = default_tstamp_;
23✔
601
        auto err = DoSanitizeLogs(log_fpath_, prep_fpath, clean_logs_, first_tstamp);
23✔
602
        if (err != error::NoError) {
23✔
603
                if (path::FileExists(prep_fpath)) {
×
604
                        auto del_err = path::FileDelete(prep_fpath);
×
605
                        if (del_err != error::NoError) {
×
606
                                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
607
                        }
608
                }
609
        } else {
610
                sanitized_fpath_ = std::move(prep_fpath);
23✔
611
                reader_ = make_unique<io::FileReader>(sanitized_fpath_);
46✔
612
                auto ex_sz = GetLogFileDataSize(sanitized_fpath_);
23✔
613
                if (!ex_sz) {
23✔
614
                        return ex_sz.error().WithContext("Failed to determine deployment logs size");
×
615
                }
616

617
                raw_data_size_ = ex_sz.value();
23✔
618
                if (raw_data_size_ > maximum_log_size_) {
23✔
619
                        large_logs_ = true;
1✔
620
                        // Make sure we end up with less data than the limit with all the
621
                        // potential extra messages added in JsonLogMessagesReader::Read()
622
                        // below.
623
                        auto ex_off = FindNextMsgAfter(
624
                                sanitized_fpath_,
625
                                (raw_data_size_ + too_much_data_msg_tmpl_.size() + bad_data_msg_tmpl_.size()
1✔
626
                                 - maximum_log_size_));
1✔
627
                        if (!ex_off) {
1✔
628
                                return ex_off.error().WithContext(
×
629
                                        "Failed to determine start offset in too large deployment logs");
×
630
                        }
631
                        reader_ = make_unique<io::FileReader>(sanitized_fpath_, ex_off.value());
2✔
632
                        raw_data_size_ -= ex_off.value();
1✔
633
                }
634
                rem_raw_data_size_ = raw_data_size_;
23✔
635
                if (!clean_logs_) {
23✔
636
                        ReplaceTimestampInMsgData(bad_data_msg_, first_tstamp, default_tstamp_.size());
9✔
637
                }
638
                if (large_logs_) {
23✔
639
                        ReplaceTimestampInMsgData(too_much_data_msg_, first_tstamp, default_tstamp_.size());
1✔
640
                }
641
        }
642
        return err;
23✔
643
}
644

645
error::Error JsonLogMessagesReader::Rewind() {
5✔
646
        AssertOrReturnError(!sanitized_fpath_.empty());
5✔
647
        header_rem_ = header_.size();
5✔
648
        closing_rem_ = closing_.size();
5✔
649
        bad_data_msg_rem_ = bad_data_msg_.size();
5✔
650
        too_much_data_msg_rem_ = too_much_data_msg_.size();
5✔
651

652
        // release/close the file first so that the FileDelete() below can actually
653
        // delete it and free space up
654
        reader_.reset();
655
        auto del_err = path::FileDelete(sanitized_fpath_);
5✔
656
        if (del_err != error::NoError) {
5✔
657
                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
2✔
658
        }
659
        sanitized_fpath_.erase();
5✔
660
        return SanitizeLogs();
5✔
661
}
662

663
int64_t JsonLogMessagesReader::TotalDataSize() {
18✔
664
        assert(!sanitized_fpath_.empty());
665

666
        auto ret = raw_data_size_ + header_.size() + closing_.size();
18✔
667
        if (!clean_logs_) {
18✔
668
                ret += bad_data_msg_.size();
9✔
669
        }
670
        if (large_logs_) {
18✔
671
                ret += too_much_data_msg_.size();
1✔
672
        }
673
        return ret;
18✔
674
}
675

676
ExpectedSize JsonLogMessagesReader::Read(
1,141✔
677
        vector<uint8_t>::iterator start, vector<uint8_t>::iterator end) {
678
        AssertOrReturnUnexpected(!sanitized_fpath_.empty());
1,141✔
679

680
        if (header_rem_ > 0) {
1,141✔
681
                io::Vsize target_size = end - start;
20✔
682
                auto copy_end = copy_n(
20✔
683
                        header_.begin() + (header_.size() - header_rem_), min(header_rem_, target_size), start);
20✔
684
                auto n_copied = copy_end - start;
685
                header_rem_ -= n_copied;
20✔
686
                return static_cast<size_t>(n_copied);
687
        } else if (large_logs_ && (too_much_data_msg_rem_ > 0)) {
1,121✔
688
                io::Vsize target_size = end - start;
1✔
689
                auto copy_end = copy_n(
1✔
690
                        too_much_data_msg_.begin() + (too_much_data_msg_.size() - too_much_data_msg_rem_),
1✔
691
                        min(too_much_data_msg_rem_, target_size),
692
                        start);
693
                auto n_copied = copy_end - start;
694
                too_much_data_msg_rem_ -= n_copied;
1✔
695
                return static_cast<size_t>(n_copied);
696
        } else if (!clean_logs_ && (bad_data_msg_rem_ > 0)) {
1,120✔
697
                io::Vsize target_size = end - start;
16✔
698
                auto copy_end = copy_n(
16✔
699
                        bad_data_msg_.begin() + (bad_data_msg_.size() - bad_data_msg_rem_),
16✔
700
                        min(bad_data_msg_rem_, target_size),
701
                        start);
702
                auto n_copied = copy_end - start;
703
                bad_data_msg_rem_ -= n_copied;
16✔
704
                return static_cast<size_t>(n_copied);
705
        } else if (rem_raw_data_size_ > 0) {
1,104✔
706
                if (end - start > rem_raw_data_size_) {
1,066✔
707
                        end = start + static_cast<size_t>(rem_raw_data_size_);
708
                }
709
                auto ex_sz = reader_->Read(start, end);
1,066✔
710
                if (!ex_sz) {
1,066✔
711
                        return ex_sz;
712
                }
713
                auto n_read = ex_sz.value();
1,066✔
714
                rem_raw_data_size_ -= n_read;
1,066✔
715

716
                // We control how much we read from the file so we should never read
717
                // 0 bytes (meaning EOF reached). If we do, it means the file is
718
                // smaller than what we were told.
719
                assert(n_read > 0);
720
                if (n_read == 0) {
1,066✔
721
                        return expected::unexpected(
×
722
                                MakeError(InvalidDataError, "Unexpected EOF when reading logs file"));
×
723
                }
724
                return n_read;
725
        } else if (closing_rem_ > 0) {
38✔
726
                io::Vsize target_size = end - start;
19✔
727
                auto copy_end = copy_n(
19✔
728
                        closing_.begin() + (closing_.size() - closing_rem_),
19✔
729
                        min(closing_rem_, target_size),
730
                        start);
731
                auto n_copied = copy_end - start;
732
                closing_rem_ -= n_copied;
19✔
733
                return static_cast<size_t>(copy_end - start);
734
        } else {
735
                return 0;
736
        }
737
};
738

739
static const string logs_uri_suffix = "/log";
740

741
error::Error DeploymentClient::PushLogs(
4✔
742
        const string &deployment_id,
743
        const string &log_file_path,
744
        api::Client &client,
745
        LogsAPIResponseHandler api_handler) {
746
        auto logs_reader = make_shared<JsonLogMessagesReader>(log_file_path);
4✔
747
        auto err = logs_reader->SanitizeLogs();
4✔
748
        if (err != error::NoError) {
4✔
749
                return err;
×
750
        }
751

752
        auto req = make_shared<api::APIRequest>();
4✔
753
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, logs_uri_suffix));
4✔
754
        req->SetMethod(http::Method::PUT);
4✔
755
        req->SetHeader("Content-Type", "application/json");
8✔
756
        req->SetHeader("Content-Length", to_string(logs_reader->TotalDataSize()));
8✔
757
        req->SetHeader("Accept", "application/json");
8✔
758
        req->SetBodyGenerator([logs_reader]() {
20✔
759
                logs_reader->Rewind();
8✔
760
                return logs_reader;
4✔
761
        });
762

763
        auto received_body = make_shared<vector<uint8_t>>();
4✔
764
        return client.AsyncCall(
16✔
765
                req,
766
                [this, received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
8✔
767
                        this->PushLogsHeaderHandler(received_body, api_handler, exp_resp);
12✔
768
                },
4✔
769
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
11✔
770
                        if (!exp_resp) {
3✔
771
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
772
                                api_handler(LogsAPIResponse {nullopt, nullopt, exp_resp.error()});
×
773
                                return;
×
774
                        }
775

776
                        auto resp = exp_resp.value();
3✔
777
                        auto status = resp->GetStatusCode();
3✔
778

779
                        // StatusTooManyRequests must have been handled in PushLogsHeaderHandler already
780
                        assert(status != http::StatusTooManyRequests);
781
                        // StatusRequestBodyTooLarge must have been handled in PushLogsHeaderHandler already
782
                        assert(status != http::StatusRequestBodyTooLarge);
783

784
                        if (status == http::StatusNoContent) {
3✔
785
                                api_handler(LogsAPIResponse {status, nullopt, error::NoError});
2✔
786
                        } else {
787
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
788
                                string err_str;
789
                                if (ex_err_msg) {
1✔
790
                                        err_str = ex_err_msg.value();
1✔
791
                                } else {
792
                                        err_str = resp->GetStatusMessage();
×
793
                                }
794
                                api_handler(LogsAPIResponse {
2✔
795
                                        status,
796
                                        nullopt,
797
                                        MakeError(
798
                                                BadResponseError,
799
                                                "Got unexpected response " + to_string(status)
1✔
800
                                                        + " from logs API: " + err_str)});
2✔
801
                        }
802
                });
4✔
803
}
804

805
void DeploymentClient::PushLogsHeaderHandler(
7✔
806
        shared_ptr<vector<uint8_t>> received_body,
807
        LogsAPIResponseHandler api_handler,
808
        http::ExpectedIncomingResponsePtr exp_resp) {
809
        if (!exp_resp) {
7✔
810
                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
811
                api_handler(LogsAPIResponse {nullopt, nullopt, exp_resp.error()});
×
812
                return;
×
813
        }
814

815
        auto body_writer = make_shared<io::ByteWriter>(received_body);
7✔
816
        auto resp = exp_resp.value();
7✔
817
        auto status = resp->GetStatusCode();
7✔
818
        if (status == http::StatusTooManyRequests) {
7✔
819
                LogsAPIResponse response = {
820
                        status, resp->GetHeaders(), MakeError(TooManyRequestsError, "Too many requests")};
3✔
821
                api_handler(response);
3✔
822
        } else if (status == http::StatusRequestBodyTooLarge) {
7✔
823
                LogsAPIResponse response = {
824
                        status,
825
                        resp->GetHeaders(),
826
                        MakeError(RequestBodyTooLargeError, "Could not send logs to server")};
1✔
827
                api_handler(response);
1✔
828
        }
1✔
829
        auto content_length = resp->GetHeader("Content-Length");
14✔
830
        if (!content_length) {
7✔
831
                log::Debug(
3✔
832
                        "Failed to get content length from the deployment log API response headers: "
833
                        + content_length.error().String());
6✔
834
                body_writer->SetUnlimited(true);
3✔
835
        } else {
836
                auto ex_len = common::StringTo<size_t>(content_length.value());
4✔
837
                if (!ex_len) {
4✔
838
                        log::Error(
×
839
                                "Failed to convert the content length from the deployment log API response headers to an integer: "
840
                                + ex_len.error().String());
×
841
                        body_writer->SetUnlimited(true);
×
842
                } else {
843
                        received_body->resize(ex_len.value());
4✔
844
                }
845
        }
846
        resp->SetBodyWriter(body_writer);
14✔
847
}
848

849
} // namespace deployments
850
} // namespace update
851
} // 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