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

mendersoftware / mender / 1504885822

21 Oct 2024 08:08AM UTC coverage: 76.257% (-0.05%) from 76.305%
1504885822

push

gitlab-ci

web-flow
Merge pull request #1676 from kacf/size_fixes

MEN-7613: Size fixes

48 of 72 new or added lines in 18 files covered. (66.67%)

3 existing lines in 2 files now uncovered.

7313 of 9590 relevant lines covered (76.26%)

11280.18 hits per line

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

76.69
/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 <mender-update/context.hpp>
33

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

38
using namespace std;
39

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

51
const DeploymentsErrorCategoryClass DeploymentsErrorCategory;
52

53
const char *DeploymentsErrorCategoryClass::name() const noexcept {
×
54
        return "DeploymentsErrorCategory";
×
55
}
56

57
string DeploymentsErrorCategoryClass::message(int code) const {
2✔
58
        switch (code) {
2✔
59
        case NoError:
60
                return "Success";
×
61
        case InvalidDataError:
62
                return "Invalid data error";
×
63
        case BadResponseError:
64
                return "Bad response error";
×
65
        case DeploymentAbortedError:
66
                return "Deployment was aborted on the server";
2✔
67
        }
68
        assert(false);
69
        return "Unknown";
×
70
}
71

72
error::Error MakeError(DeploymentsErrorCode code, const string &msg) {
25✔
73
        return error::Error(error_condition(code, DeploymentsErrorCategory), msg);
30✔
74
}
75

76
static const string check_updates_v1_uri = "/api/devices/v1/deployments/device/deployments/next";
77
static const string check_updates_v2_uri = "/api/devices/v2/deployments/device/deployments/next";
78

79
error::Error DeploymentClient::CheckNewDeployments(
7✔
80
        context::MenderContext &ctx, api::Client &client, CheckUpdatesAPIResponseHandler api_handler) {
81
        auto ex_dev_type = ctx.GetDeviceType();
7✔
82
        if (!ex_dev_type) {
7✔
83
                return ex_dev_type.error();
1✔
84
        }
85
        string device_type = ex_dev_type.value();
6✔
86

87
        auto ex_provides = ctx.LoadProvides();
6✔
88
        if (!ex_provides) {
6✔
89
                return ex_provides.error();
×
90
        }
91
        auto provides = ex_provides.value();
6✔
92
        if (provides.find("artifact_name") == provides.end()) {
12✔
93
                return MakeError(InvalidDataError, "Missing artifact name data");
×
94
        }
95

96
        stringstream ss;
12✔
97
        ss << R"({"device_provides":{)";
6✔
98
        ss << R"("device_type":")";
6✔
99
        ss << json::EscapeString(device_type);
12✔
100

101
        for (const auto &kv : provides) {
14✔
102
                ss << "\",\"" + json::EscapeString(kv.first) + "\":\"";
16✔
103
                ss << json::EscapeString(kv.second);
16✔
104
        }
105

106
        ss << R"("}})";
6✔
107

108
        string v2_payload = ss.str();
109
        log::Debug("deployments/next v2 payload " + v2_payload);
6✔
110
        http::BodyGenerator payload_gen = [v2_payload]() {
48✔
111
                return make_shared<io::StringReader>(v2_payload);
6✔
112
        };
6✔
113

114
        auto v2_req = make_shared<api::APIRequest>();
6✔
115
        v2_req->SetPath(check_updates_v2_uri);
116
        v2_req->SetMethod(http::Method::POST);
6✔
117
        v2_req->SetHeader("Content-Type", "application/json");
12✔
118
        v2_req->SetHeader("Content-Length", to_string(v2_payload.size()));
12✔
119
        v2_req->SetHeader("Accept", "application/json");
12✔
120
        v2_req->SetBodyGenerator(payload_gen);
6✔
121

122
        string v1_args = "artifact_name=" + http::URLEncode(provides["artifact_name"])
18✔
123
                                         + "&device_type=" + http::URLEncode(device_type);
18✔
124
        auto v1_req = make_shared<api::APIRequest>();
6✔
125
        v1_req->SetPath(check_updates_v1_uri + "?" + v1_args);
12✔
126
        v1_req->SetMethod(http::Method::GET);
6✔
127
        v1_req->SetHeader("Accept", "application/json");
12✔
128

129
        auto received_body = make_shared<vector<uint8_t>>();
6✔
130
        auto handle_data = [received_body, api_handler](unsigned status) {
4✔
131
                if (status == http::StatusOK) {
4✔
132
                        auto ex_j = json::Load(common::StringFromByteVector(*received_body));
4✔
133
                        if (ex_j) {
2✔
134
                                CheckUpdatesAPIResponse response {optional<json::Json> {ex_j.value()}};
2✔
135
                                api_handler(response);
4✔
136
                        } else {
137
                                api_handler(expected::unexpected(ex_j.error()));
×
138
                        }
139
                } else if (status == http::StatusNoContent) {
2✔
140
                        api_handler(CheckUpdatesAPIResponse {nullopt});
4✔
141
                } else {
142
                        log::Warning(
×
143
                                "DeploymentClient::CheckNewDeployments - received unhandled http response: "
144
                                + to_string(status));
×
145
                        api_handler(expected::unexpected(MakeError(
×
146
                                DeploymentAbortedError, "received unhandled HTTP response: " + to_string(status))));
×
147
                }
148
        };
16✔
149

150
        http::ResponseHandler header_handler =
151
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
9✔
152
                        if (!exp_resp) {
9✔
153
                                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
154
                                CheckUpdatesAPIResponse response = expected::unexpected(exp_resp.error());
×
155
                                api_handler(response);
×
156
                                return;
157
                        }
158

159
                        auto resp = exp_resp.value();
9✔
160
                        received_body->clear();
9✔
161
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
9✔
162
                        body_writer->SetUnlimited(true);
9✔
163
                        resp->SetBodyWriter(body_writer);
18✔
164
                };
12✔
165

166
        http::ResponseHandler v1_body_handler =
167
                [received_body, api_handler, handle_data](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
168
                        if (!exp_resp) {
3✔
169
                                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
170
                                CheckUpdatesAPIResponse response = expected::unexpected(exp_resp.error());
×
171
                                api_handler(response);
×
172
                                return;
173
                        }
174
                        auto resp = exp_resp.value();
3✔
175
                        auto status = resp->GetStatusCode();
3✔
176
                        if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
3✔
177
                                handle_data(status);
2✔
178
                        } else {
179
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
180
                                string err_str;
181
                                if (ex_err_msg) {
1✔
182
                                        err_str = ex_err_msg.value();
×
183
                                } else {
184
                                        err_str = resp->GetStatusMessage();
2✔
185
                                }
186
                                api_handler(expected::unexpected(MakeError(
2✔
187
                                        BadResponseError,
188
                                        "Got unexpected response " + to_string(status) + ": " + err_str)));
4✔
189
                        }
190
                };
12✔
191

192
        http::ResponseHandler v2_body_handler = [received_body,
6✔
193
                                                                                         v1_req,
194
                                                                                         header_handler,
195
                                                                                         v1_body_handler,
196
                                                                                         api_handler,
197
                                                                                         handle_data,
198
                                                                                         &client](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
199
                if (!exp_resp) {
6✔
200
                        log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
201
                        CheckUpdatesAPIResponse response = expected::unexpected(exp_resp.error());
×
202
                        api_handler(response);
×
203
                        return;
204
                }
205
                auto resp = exp_resp.value();
6✔
206
                auto status = resp->GetStatusCode();
6✔
207
                if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
6✔
208
                        handle_data(status);
2✔
209
                } else if (status == http::StatusNotFound) {
4✔
210
                        log::Debug(
3✔
211
                                "POST request to v2 version of the deployments API failed, falling back to v1 version and GET");
6✔
212
                        auto err = client.AsyncCall(v1_req, header_handler, v1_body_handler);
9✔
213
                        if (err != error::NoError) {
3✔
214
                                api_handler(expected::unexpected(err.WithContext("While calling v1 endpoint")));
×
215
                        }
216
                } else {
217
                        auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
218
                        string err_str;
219
                        if (ex_err_msg) {
1✔
220
                                err_str = ex_err_msg.value();
1✔
221
                        } else {
222
                                err_str = resp->GetStatusMessage();
×
223
                        }
224
                        api_handler(expected::unexpected(MakeError(
2✔
225
                                BadResponseError,
226
                                "Got unexpected response " + to_string(status) + ": " + err_str)));
4✔
227
                }
228
        };
6✔
229

230
        return client.AsyncCall(v2_req, header_handler, v2_body_handler);
18✔
231
}
232

233
static const string deployment_status_strings[static_cast<int>(DeploymentStatus::End_) + 1] = {
234
        "installing",
235
        "pause_before_installing",
236
        "downloading",
237
        "pause_before_rebooting",
238
        "rebooting",
239
        "pause_before_committing",
240
        "success",
241
        "failure",
242
        "already-installed"};
243

244
static const string deployments_uri_prefix = "/api/devices/v1/deployments/device/deployments";
245
static const string status_uri_suffix = "/status";
246

247
string DeploymentStatusString(DeploymentStatus status) {
494✔
248
        return deployment_status_strings[static_cast<int>(status)];
498✔
249
}
250

251
error::Error DeploymentClient::PushStatus(
4✔
252
        const string &deployment_id,
253
        DeploymentStatus status,
254
        const string &substate,
255
        api::Client &client,
256
        StatusAPIResponseHandler api_handler) {
257
        // Cannot push a status update without a deployment ID
258
        AssertOrReturnError(deployment_id != "");
4✔
259
        string payload = R"({"status":")" + DeploymentStatusString(status) + "\"";
8✔
260
        if (substate != "") {
4✔
261
                payload += R"(,"substate":")" + json::EscapeString(substate) + "\"}";
6✔
262
        } else {
263
                payload += "}";
1✔
264
        }
265
        http::BodyGenerator payload_gen = [payload]() {
32✔
266
                return make_shared<io::StringReader>(payload);
4✔
267
        };
4✔
268

269
        auto req = make_shared<api::APIRequest>();
4✔
270
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, status_uri_suffix));
8✔
271
        req->SetMethod(http::Method::PUT);
4✔
272
        req->SetHeader("Content-Type", "application/json");
8✔
273
        req->SetHeader("Content-Length", to_string(payload.size()));
8✔
274
        req->SetHeader("Accept", "application/json");
8✔
275
        req->SetBodyGenerator(payload_gen);
4✔
276

277
        auto received_body = make_shared<vector<uint8_t>>();
4✔
278
        return client.AsyncCall(
279
                req,
280
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
4✔
281
                        if (!exp_resp) {
4✔
282
                                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
283
                                api_handler(exp_resp.error());
×
284
                                return;
×
285
                        }
286

287
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
4✔
288
                        auto resp = exp_resp.value();
4✔
289
                        auto content_length = resp->GetHeader("Content-Length");
8✔
290
                        if (!content_length) {
4✔
291
                                log::Debug(
×
292
                                        "Failed to get content length from the status API response headers: "
293
                                        + content_length.error().String());
×
294
                                body_writer->SetUnlimited(true);
×
295
                        } else {
296
                                auto ex_len = common::StringToLongLong(content_length.value());
4✔
297
                                if (!ex_len) {
4✔
298
                                        log::Error(
×
299
                                                "Failed to convert the content length from the status API response headers to an integer: "
300
                                                + ex_len.error().String());
×
301
                                        body_writer->SetUnlimited(true);
×
302
                                } else if (
303
                                        ex_len.value() < 0
4✔
304
                                        or static_cast<unsigned long long>(ex_len.value())
4✔
305
                                                   > numeric_limits<size_t>::max()) {
306
                                        // This is a ridiculuous limit, but we are mainly interested
307
                                        // in catching corrupt data / mistakes here. Actually
308
                                        // limiting memory usage in a useful way is something which
309
                                        // should be thought through more carefully and maybe
310
                                        // configurable.
NEW
311
                                        api_handler(error::Error(
×
NEW
312
                                                make_error_condition(errc::result_out_of_range),
×
NEW
313
                                                "Content-Length out of range"));
×
314
                                        return;
315
                                } else {
316
                                        received_body->resize(static_cast<size_t>(ex_len.value()));
4✔
317
                                }
318
                        }
319
                        resp->SetBodyWriter(body_writer);
8✔
320
                },
321
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
4✔
322
                        if (!exp_resp) {
4✔
323
                                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
324
                                api_handler(exp_resp.error());
×
325
                                return;
×
326
                        }
327

328
                        auto resp = exp_resp.value();
4✔
329
                        auto status = resp->GetStatusCode();
4✔
330
                        if (status == http::StatusNoContent) {
4✔
331
                                api_handler(error::NoError);
4✔
332
                        } else if (status == http::StatusConflict) {
2✔
333
                                api_handler(
1✔
334
                                        MakeError(DeploymentAbortedError, "Could not send status update to server"));
2✔
335
                        } else {
336
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
337
                                string err_str;
338
                                if (ex_err_msg) {
1✔
339
                                        err_str = ex_err_msg.value();
1✔
340
                                } else {
341
                                        err_str = resp->GetStatusMessage();
×
342
                                }
343
                                api_handler(MakeError(
2✔
344
                                        BadResponseError,
345
                                        "Got unexpected response " + to_string(status)
2✔
346
                                                + " from status API: " + err_str));
2✔
347
                        }
348
                });
16✔
349
}
350

351
using mender::common::expected::ExpectedSize;
352

353
static ExpectedSize GetLogFileDataSize(const string &path) {
3✔
354
        auto ex_istr = io::OpenIfstream(path);
3✔
355
        if (!ex_istr) {
3✔
356
                return expected::unexpected(ex_istr.error());
×
357
        }
358
        auto istr = std::move(ex_istr.value());
6✔
359

360
        // We want the size of the actual data without a potential trailing
361
        // newline. So let's seek one byte before the end of file, check if the last
362
        // byte is a newline and return the appropriate number.
363
        istr.seekg(-1, ios_base::end);
3✔
364
        int c = istr.get();
3✔
365
        if (c == '\n') {
3✔
366
                return istr.tellg() - static_cast<ifstream::off_type>(1);
3✔
367
        } else {
368
                return istr.tellg();
×
369
        }
370
}
371

372
const vector<uint8_t> JsonLogMessagesReader::header_ = {
373
        '{', '"', 'm', 'e', 's', 's', 'a', 'g', 'e', 's', '"', ':', '['};
374
const vector<uint8_t> JsonLogMessagesReader::closing_ = {']', '}'};
375

376
ExpectedSize JsonLogMessagesReader::Read(
89✔
377
        vector<uint8_t>::iterator start, vector<uint8_t>::iterator end) {
378
        if (header_rem_ > 0) {
89✔
379
                io::Vsize target_size = end - start;
9✔
380
                auto copy_end = copy_n(
381
                        header_.begin() + (header_.size() - header_rem_), min(header_rem_, target_size), start);
10✔
382
                auto n_copied = copy_end - start;
383
                header_rem_ -= n_copied;
9✔
384
                return static_cast<size_t>(n_copied);
385
        } else if (rem_raw_data_size_ > 0) {
80✔
386
                if (end - start > rem_raw_data_size_) {
64✔
387
                        end = start + static_cast<size_t>(rem_raw_data_size_);
388
                }
389
                auto ex_sz = reader_->Read(start, end);
64✔
390
                if (!ex_sz) {
64✔
391
                        return ex_sz;
392
                }
393
                auto n_read = ex_sz.value();
64✔
394
                rem_raw_data_size_ -= n_read;
64✔
395

396
                // We control how much we read from the file so we should never read
397
                // 0 bytes (meaning EOF reached). If we do, it means the file is
398
                // smaller than what we were told.
399
                assert(n_read > 0);
400
                if (n_read == 0) {
64✔
401
                        return expected::unexpected(
×
402
                                MakeError(InvalidDataError, "Unexpected EOF when reading logs file"));
×
403
                }
404

405
                // Replace all newlines with commas
406
                const auto read_end = start + n_read;
407
                for (auto it = start; it < read_end; it++) {
1,916✔
408
                        if (it[0] == '\n') {
1,852✔
409
                                it[0] = ',';
12✔
410
                        }
411
                }
412
                return n_read;
413
        } else if (closing_rem_ > 0) {
16✔
414
                io::Vsize target_size = end - start;
8✔
415
                auto copy_end = copy_n(
416
                        closing_.begin() + (closing_.size() - closing_rem_),
8✔
417
                        min(closing_rem_, target_size),
8✔
418
                        start);
8✔
419
                auto n_copied = copy_end - start;
420
                closing_rem_ -= n_copied;
8✔
421
                return static_cast<size_t>(copy_end - start);
422
        } else {
423
                return 0;
424
        }
425
};
426

427
static const string logs_uri_suffix = "/log";
428

429
error::Error DeploymentClient::PushLogs(
3✔
430
        const string &deployment_id,
431
        const string &log_file_path,
432
        api::Client &client,
433
        LogsAPIResponseHandler api_handler) {
434
        auto ex_size = GetLogFileDataSize(log_file_path);
3✔
435
        if (!ex_size) {
3✔
436
                // api_handler(ex_size.error()) ???
437
                return ex_size.error();
×
438
        }
439
        auto data_size = ex_size.value();
3✔
440

441
        auto file_reader = make_shared<io::FileReader>(log_file_path);
3✔
442
        auto logs_reader = make_shared<JsonLogMessagesReader>(file_reader, data_size);
3✔
443

444
        auto req = make_shared<api::APIRequest>();
3✔
445
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, logs_uri_suffix));
6✔
446
        req->SetMethod(http::Method::PUT);
3✔
447
        req->SetHeader("Content-Type", "application/json");
6✔
448
        req->SetHeader("Content-Length", to_string(JsonLogMessagesReader::TotalDataSize(data_size)));
6✔
449
        req->SetHeader("Accept", "application/json");
6✔
450
        req->SetBodyGenerator([logs_reader]() {
18✔
451
                logs_reader->Rewind();
6✔
452
                return logs_reader;
3✔
453
        });
6✔
454

455
        auto received_body = make_shared<vector<uint8_t>>();
3✔
456
        return client.AsyncCall(
457
                req,
458
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
459
                        if (!exp_resp) {
3✔
460
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
461
                                api_handler(exp_resp.error());
×
462
                                return;
×
463
                        }
464

465
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
3✔
466
                        auto resp = exp_resp.value();
3✔
467
                        auto content_length = resp->GetHeader("Content-Length");
6✔
468
                        if (!content_length) {
3✔
469
                                log::Debug(
×
470
                                        "Failed to get content length from the status API response headers: "
471
                                        + content_length.error().String());
×
472
                                body_writer->SetUnlimited(true);
×
473
                        } else {
474
                                auto ex_len = common::StringToLongLong(content_length.value());
3✔
475
                                if (!ex_len) {
3✔
476
                                        log::Error(
×
477
                                                "Failed to convert the content length from the status API response headers to an integer: "
478
                                                + ex_len.error().String());
×
479
                                        body_writer->SetUnlimited(true);
×
480
                                } else if (
481
                                        ex_len.value() < 0
3✔
482
                                        or static_cast<unsigned long long>(ex_len.value())
3✔
483
                                                   > numeric_limits<size_t>::max()) {
484
                                        // This is a ridiculuous limit, but we are mainly interested
485
                                        // in catching corrupt data / mistakes here. Actually
486
                                        // limiting memory usage in a useful way is something which
487
                                        // should be thought through more carefully and maybe
488
                                        // configurable.
NEW
489
                                        api_handler(error::Error(
×
NEW
490
                                                make_error_condition(errc::result_out_of_range),
×
NEW
491
                                                "Content-Length out of range"));
×
492
                                        return;
493
                                } else {
494
                                        received_body->resize(static_cast<size_t>(ex_len.value()));
3✔
495
                                }
496
                        }
497
                        resp->SetBodyWriter(body_writer);
6✔
498
                },
499
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
500
                        if (!exp_resp) {
3✔
501
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
502
                                api_handler(exp_resp.error());
×
503
                                return;
×
504
                        }
505

506
                        auto resp = exp_resp.value();
3✔
507
                        auto status = resp->GetStatusCode();
3✔
508
                        if (status == http::StatusNoContent) {
3✔
509
                                api_handler(error::NoError);
4✔
510
                        } else {
511
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
512
                                string err_str;
513
                                if (ex_err_msg) {
1✔
514
                                        err_str = ex_err_msg.value();
1✔
515
                                } else {
516
                                        err_str = resp->GetStatusMessage();
×
517
                                }
518
                                api_handler(MakeError(
2✔
519
                                        BadResponseError,
520
                                        "Got unexpected response " + to_string(status) + " from logs API: " + err_str));
2✔
521
                        }
522
                });
12✔
523
}
524

525
} // namespace deployments
526
} // namespace update
527
} // 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

© 2025 Coveralls, Inc