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

mendersoftware / mender / 2053907689

22 Sep 2025 12:54PM UTC coverage: 79.863%. First build
2053907689

push

gitlab-ci

danielskinstad
feat: implement system_type support for System devices

System_type is used in two specific cases:
1) when polling for deployments
2) when installing manifest artifacts (payload type "mender-orchestrator-manifest")

Regular artifacts (e.g. rootfs updates) continue using device_type even on
System devices, but note that these are installed through mender-orchestrator
since deployment polling uses system_type.

Ticket: MEN-8650
Changelog: Add system_type support for System devices. Devices with
DeviceTier set to "system" use system_type from topology.yaml instead
of device_type for deployment polling and manifest artifact compatibility
checking.

Signed-off-by: Daniel Skinstad Drabitzius <daniel.drabitzius@northern.tech>

33 of 39 new or added lines in 2 files covered. (84.62%)

7801 of 9768 relevant lines covered (79.86%)

14045.86 hits per line

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

84.0
/src/mender-update/context/context.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/context.hpp>
16

17
#include <cctype>
18

19
#include <algorithm>
20
#include <set>
21

22
#include <artifact/artifact.hpp>
23
#include <common/common.hpp>
24
#include <common/error.hpp>
25
#include <common/expected.hpp>
26
#include <common/io.hpp>
27
#include <common/json.hpp>
28
#include <common/key_value_database.hpp>
29
#include <common/log.hpp>
30
#include <common/path.hpp>
31

32
#ifdef MENDER_USE_YAML_CPP
33
#include <common/yaml.hpp>
34
#endif // MENDER_USE_YAML_CPP
35

36
namespace mender {
37
namespace update {
38
namespace context {
39

40
using namespace std;
41
namespace artifact = mender::artifact;
42
namespace common = mender::common;
43
namespace error = mender::common::error;
44
namespace expected = mender::common::expected;
45
namespace io = mender::common::io;
46
namespace json = mender::common::json;
47
namespace kv_db = mender::common::key_value_database;
48
namespace log = mender::common::log;
49
namespace path = mender::common::path;
50
namespace yaml = mender::common::yaml;
51

52
const string MenderContext::broken_artifact_name_suffix {"_INCONSISTENT"};
53

54
const string MenderContext::artifact_name_key {"artifact-name"};
55
const string MenderContext::artifact_group_key {"artifact-group"};
56
const string MenderContext::artifact_provides_key {"artifact-provides"};
57
const string MenderContext::standalone_state_key {"standalone-state"};
58
const string MenderContext::state_data_key {"state"};
59
const string MenderContext::state_data_key_uncommitted {"state-uncommitted"};
60
const string MenderContext::update_control_maps {"update-control-maps"};
61
const string MenderContext::auth_token_name {"authtoken"};
62
const string MenderContext::auth_token_cache_invalidator_name {"auth-token-cache-invalidator"};
63
const string MenderContext::orchestrator_manifest_payload_type {"mender-orchestrator-manifest"};
64

65
const int MenderContext::standalone_data_version {2};
66

67
const MenderContextErrorCategoryClass MenderContextErrorCategory;
68

69
const char *MenderContextErrorCategoryClass::name() const noexcept {
×
70
        return "MenderContextErrorCategory";
×
71
}
72

73
string MenderContextErrorCategoryClass::message(int code) const {
3✔
74
        switch (code) {
3✔
75
        case NoError:
76
                return "Success";
×
77
        case ParseError:
78
                return "Parse error";
×
79
        case ValueError:
80
                return "Value error";
×
81
        case NoSuchUpdateModuleError:
82
                return "Update Module not found for given artifact type";
×
83
        case DatabaseValueError:
84
                return "Value in database is invalid or corrupted";
×
85
        case RebootRequiredError:
86
                return "Reboot required";
×
87
        case NoUpdateInProgressError:
88
                return "No update in progress";
2✔
89
        case UnexpectedHttpResponse:
90
                return "Unexpected HTTP response";
×
91
        case StateDataStoreCountExceededError:
92
                return "State data store count exceeded";
1✔
93
        case WrongOperationError:
94
                return "Operation cannot be done in this state";
×
95
        }
96
        assert(false);
97
        return "Unknown";
×
98
}
99

100
error::Error MakeError(MenderContextErrorCode code, const string &msg) {
376✔
101
        return error::Error(error_condition(code, MenderContextErrorCategory), msg);
382✔
102
}
103

104
error::Error MenderContext::Initialize() {
441✔
105
#ifdef MENDER_USE_LMDB
106
        auto err = mender_store_.Open(path::Join(config_.paths.GetDataStore(), "mender-store"));
882✔
107
        if (error::NoError != err) {
441✔
108
                return err;
1✔
109
        }
110
        err = mender_store_.Remove(auth_token_name);
440✔
111
        if (error::NoError != err) {
440✔
112
                // key not existing in the DB is not treated as an error so this must be
113
                // a real error
114
                return err;
×
115
        }
116
        err = mender_store_.Remove(auth_token_cache_invalidator_name);
440✔
117
        if (error::NoError != err) {
440✔
118
                // same as above -- a real error
119
                return err;
×
120
        }
121

122
        return error::NoError;
440✔
123
#else
124
        return error::NoError;
125
#endif
126
}
127

128
kv_db::KeyValueDatabase &MenderContext::GetMenderStoreDB() {
1,738✔
129
        return mender_store_;
1,738✔
130
}
131

132
ExpectedProvidesData MenderContext::LoadProvides() {
415✔
133
        ExpectedProvidesData data;
134
        auto err = mender_store_.ReadTransaction([this, &data](kv_db::Transaction &txn) {
414✔
135
                data = LoadProvides(txn);
828✔
136
                if (!data) {
414✔
137
                        return data.error();
2✔
138
                }
139
                return error::NoError;
412✔
140
        });
415✔
141
        if (err != error::NoError) {
415✔
142
                return expected::unexpected(err);
6✔
143
        }
144
        return data;
145
}
146

147
ExpectedProvidesData MenderContext::LoadProvides(kv_db::Transaction &txn) {
508✔
148
        string artifact_name;
149
        string artifact_group;
150
        string artifact_provides_str;
151

152
        auto err = kv_db::ReadString(txn, artifact_name_key, artifact_name, true);
508✔
153
        if (err != error::NoError) {
508✔
154
                return expected::unexpected(err);
×
155
        }
156
        err = kv_db::ReadString(txn, artifact_group_key, artifact_group, true);
508✔
157
        if (err != error::NoError) {
508✔
158
                return expected::unexpected(err);
×
159
        }
160
        err = kv_db::ReadString(txn, artifact_provides_key, artifact_provides_str, true);
508✔
161
        if (err != error::NoError) {
508✔
162
                return expected::unexpected(err);
×
163
        }
164

165
        ProvidesData ret {};
508✔
166
        if (artifact_name != "") {
508✔
167
                ret["artifact_name"] = artifact_name;
1,000✔
168
        }
169
        if (artifact_group != "") {
508✔
170
                ret["artifact_group"] = artifact_group;
20✔
171
        }
172
        if (artifact_provides_str == "") {
508✔
173
                // nothing more to do
174
                return ret;
388✔
175
        }
176

177
        auto ex_j = json::Load(artifact_provides_str);
240✔
178
        if (!ex_j) {
120✔
179
                return expected::unexpected(ex_j.error());
2✔
180
        }
181
        auto ex_children = ex_j.value().GetChildren();
119✔
182
        if (!ex_children) {
119✔
183
                return expected::unexpected(ex_children.error());
×
184
        }
185

186
        auto children = ex_children.value();
119✔
187
        if (!all_of(children.cbegin(), children.cend(), [](const json::ChildrenMap::value_type &it) {
119✔
188
                        return it.second.IsString();
213✔
189
                })) {
190
                auto err = json::MakeError(json::TypeError, "Unexpected non-string data in provides");
2✔
191
                return expected::unexpected(err);
2✔
192
        }
193
        for (const auto &it : ex_children.value()) {
330✔
194
                ret[it.first] = it.second.GetString().value();
424✔
195
        }
196

197
        return ret;
118✔
198
}
199

200
#ifdef MENDER_USE_YAML_CPP
201
expected::ExpectedString MenderContext::GetSystemType() {
4✔
202
        string topology_dir =
203
                conf::GetEnv("MENDER_ORCHESTRATOR_TOPOLOGY_DIR", "/var/lib/mender-orchestrator/");
8✔
204
        string topology_file_path = path::Join(topology_dir, "topology.yaml");
4✔
205

206
        auto ex_topology = yaml::LoadFromFile(topology_file_path);
8✔
207
        if (!ex_topology) {
4✔
208
                return expected::unexpected(MakeError(
1✔
209
                        ParseError,
210
                        "Failed to load topology file '" + topology_file_path
2✔
211
                                + "': " + ex_topology.error().message));
3✔
212
        }
213

214
        auto ex_system_type = ex_topology.value().Get("system_type");
3✔
215
        if (!ex_system_type) {
3✔
NEW
216
                return expected::unexpected(MakeError(
×
217
                        ParseError,
NEW
218
                        "Failed to find 'system_type' in topology file '" + topology_file_path
×
NEW
219
                                + "': " + ex_system_type.error().message));
×
220
        }
221

222
        auto ex_system_type_str = ex_system_type.value().Get<string>();
3✔
223
        if (!ex_system_type_str) {
3✔
NEW
224
                return expected::unexpected(MakeError(
×
225
                        ParseError,
NEW
226
                        "Failed to parse 'system_type' as string in topology file '" + topology_file_path
×
NEW
227
                                + "': " + ex_system_type_str.error().message));
×
228
        }
229

230
        return ex_system_type_str.value();
3✔
231
}
232
#endif // MENDER_USE_YAML_CPP
233

234
expected::ExpectedString MenderContext::GetDeviceType() {
297✔
235
        string device_type_fpath;
236
        if (config_.device_type_file != "") {
297✔
237
                device_type_fpath = config_.device_type_file;
3✔
238
        } else {
239
                device_type_fpath = path::Join(config_.paths.GetDataStore(), "device_type");
588✔
240
        }
241
        auto ex_is = io::OpenIfstream(device_type_fpath);
297✔
242
        if (!ex_is) {
297✔
243
                return expected::ExpectedString(expected::unexpected(ex_is.error()));
12✔
244
        }
245

246
        auto &is = ex_is.value();
291✔
247
        string line;
248
        errno = 0;
291✔
249
        getline(is, line);
291✔
250
        if (is.bad()) {
291✔
251
                int io_errno = errno;
×
252
                error::Error err {
253
                        generic_category().default_error_condition(io_errno),
×
254
                        "Failed to read device type from '" + device_type_fpath + "'"};
×
255
                return expected::ExpectedString(expected::unexpected(err));
×
256
        }
257

258
        const string::size_type eq_pos = 12;
259
        if (line.substr(0, eq_pos) != "device_type=") {
582✔
260
                auto err = MakeError(ParseError, "Failed to parse device_type data '" + line + "'");
6✔
261
                return expected::ExpectedString(expected::unexpected(err));
6✔
262
        }
263

264
        string ret = line.substr(eq_pos, string::npos);
288✔
265

266
        if (!is.eof()) {
288✔
267
                errno = 0;
287✔
268
                getline(is, line);
287✔
269
                if ((line != "") || (!is.eof())) {
287✔
270
                        auto err = MakeError(ValueError, "Trailing device_type data");
4✔
271
                        return expected::ExpectedString(expected::unexpected(err));
4✔
272
                }
273
        }
274

275
        return expected::ExpectedString(ret);
286✔
276
}
277

278
// This function determines whether we return the system_type from the topology
279
// or the device_type. The system_type should be used rather than the device_type in two places:
280
//   1) When we poll for a deployment and the device_tier is set to `system`
281
//   2) When matching the artifact context when installing a mender-orchestrator manifest
282
expected::ExpectedString MenderContext::GetCompatibleType(const string &payload_type) {
125✔
283
        if (config_.device_tier == "system") {
125✔
284
#ifdef MENDER_USE_YAML_CPP
285
                if (payload_type == "") {
4✔
286
                        // Deployment polling -> use system_type
287
                        return GetSystemType();
2✔
288
                } else if (payload_type == orchestrator_manifest_payload_type) {
2✔
289
                        // Manifest installtion -> use system_type
290
                        return GetSystemType();
1✔
291
                } else {
292
                        // Regular artifact processing (called by orchestrator) -> use device_type
293
                        return GetDeviceType();
1✔
294
                }
295
#else
296
                return expected::unexpected(MakeError(
297
                        ValueError, "DeviceTier is 'system', but MENDER_USE_YAML_CPP is not enabled"));
298
#endif // MENDER_USE_YAML_CPP
299
        }
300

301
        // Regular device, always use device_type
302
        return GetDeviceType();
121✔
303
}
304

305
bool CheckClearsMatch(const string &to_match, const string &clears_string) {
275✔
306
        if (clears_string.empty()) {
275✔
307
                return to_match.empty();
10✔
308
        }
309

310
        vector<std::string> sub_strings;
265✔
311
        string escaped;
312
        for (const auto chr : clears_string) {
4,342✔
313
                if (chr == '*') {
4,077✔
314
                        sub_strings.push_back(escaped);
144✔
315
                        escaped.clear();
316
                } else {
317
                        escaped.push_back(chr);
3,933✔
318
                }
319
        }
320
        sub_strings.push_back(escaped);
265✔
321

322
        // Make sure that that front of vector starts at index 0
323
        if (sub_strings.front() != ""
265✔
324
                && to_match.compare(0, sub_strings.front().size(), sub_strings.front()) != 0) {
265✔
325
                return false;
326
        }
327
        // Checks if no trailing wildcard
328
        if (sub_strings.back() != ""
58✔
329
                && to_match.compare(
58✔
330
                           to_match.size() - sub_strings.back().size(), to_match.size(), sub_strings.back())
331
                           != 0) {
332
                return false;
333
        }
334

335
        // Iterate over substrings, set boundary if found to avoid
336
        // matching same substring twice
337
        size_t boundary = 0;
338
        for (const auto &str : sub_strings) {
156✔
339
                if (!str.empty()) {
109✔
340
                        size_t find = to_match.find(str, boundary);
57✔
341
                        if (find == string::npos) {
57✔
342
                                return false;
343
                        }
344
                        boundary = find + str.size();
55✔
345
                }
346
        }
347
        return true;
348
}
349

350
error::Error FilterProvides(
93✔
351
        const ProvidesData &new_provides,
352
        const ClearsProvidesData &clears_provides,
353
        ProvidesData &to_modify) {
354
        // Use clears_provides to filter out unwanted provides.
355
        for (auto to_clear : clears_provides) {
257✔
356
                set<string> keys;
357
                for (auto provide : to_modify) {
658✔
358
                        if (CheckClearsMatch(provide.first, to_clear)) {
247✔
359
                                keys.insert(provide.first);
32✔
360
                        }
361
                }
362
                for (auto key : keys) {
196✔
363
                        to_modify.erase(key);
32✔
364
                }
365
        }
366

367
        // Now add the provides from the new_provides set.
368
        for (auto provide : new_provides) {
335✔
369
                to_modify[provide.first] = provide.second;
370
        }
371

372
        return error::NoError;
93✔
373
}
374

375
error::Error MenderContext::CommitArtifactData(
94✔
376
        string artifact_name,
377
        string artifact_group,
378
        const optional<ProvidesData> &new_provides,
379
        const optional<ClearsProvidesData> &clears_provides,
380
        function<error::Error(kv_db::Transaction &)> txn_func) {
381
        return mender_store_.WriteTransaction([&](kv_db::Transaction &txn) {
94✔
382
                auto exp_existing = LoadProvides(txn);
94✔
383
                if (!exp_existing) {
94✔
384
                        return exp_existing.error();
×
385
                }
386
                auto modified_provides = exp_existing.value();
94✔
387

388
                error::Error err;
94✔
389
                if (!new_provides && !clears_provides) {
161✔
390
                        // Neither provides nor clear_provides came with the artifact. This means
391
                        // erase everything. `artifact_name` and `artifact_group` will still be
392
                        // preserved through special cases below.
393
                        modified_provides.clear();
394
                } else if (!new_provides) {
88✔
395
                        // No new provides came with the artifact. This means filter what we have,
396
                        // but don't add any new provides fields.
397
                        ProvidesData empty_provides;
398
                        err = FilterProvides(empty_provides, clears_provides.value(), modified_provides);
42✔
399
                } else if (!clears_provides) {
67✔
400
                        // Missing clears_provides is equivalent to `["*"]`, for historical reasons.
401
                        modified_provides = new_provides.value();
3✔
402
                } else {
403
                        // Standard case, filter existing provides using clears_provides, and then
404
                        // add new ones on top.
405
                        err = FilterProvides(new_provides.value(), clears_provides.value(), modified_provides);
128✔
406
                }
407
                if (err != error::NoError) {
94✔
408
                        return err;
×
409
                }
410

411
                if (artifact_name != "") {
94✔
412
                        modified_provides["artifact_name"] = artifact_name;
188✔
413
                }
414
                if (artifact_group != "") {
94✔
415
                        modified_provides["artifact_group"] = artifact_group;
8✔
416
                }
417

418
                string artifact_provides_str {"{"};
94✔
419
                for (const auto &it : modified_provides) {
308✔
420
                        if (it.first != "artifact_name" && it.first != "artifact_group") {
214✔
421
                                artifact_provides_str += "\"" + json::EscapeString(it.first) + "\":" + "\""
230✔
422
                                                                                 + json::EscapeString(it.second) + "\",";
460✔
423
                        }
424
                }
425

426
                // if some key-value pairs were added, replace the trailing comma with the
427
                // closing '}' to make a valid JSON
428
                if (artifact_provides_str != "{") {
94✔
429
                        artifact_provides_str[artifact_provides_str.length() - 1] = '}';
68✔
430
                } else {
431
                        // set to an empty value for consistency with the other two items
432
                        artifact_provides_str = "";
26✔
433
                }
434

435
                if (modified_provides["artifact_name"] != "") {
282✔
436
                        err = txn.Write(
94✔
437
                                artifact_name_key,
438
                                common::ByteVectorFromString(modified_provides["artifact_name"]));
282✔
439
                        if (err != error::NoError) {
94✔
440
                                return err;
×
441
                        }
442
                } else {
443
                        // This should not happen.
444
                        AssertOrReturnError(false);
×
445
                }
446

447
                if (modified_provides["artifact_group"] != "") {
282✔
448
                        err = txn.Write(
5✔
449
                                artifact_group_key,
450
                                common::ByteVectorFromString(modified_provides["artifact_group"]));
15✔
451
                } else {
452
                        err = txn.Remove(artifact_group_key);
178✔
453
                }
454
                if (err != error::NoError) {
94✔
455
                        return err;
×
456
                }
457

458
                if (artifact_provides_str != "") {
94✔
459
                        err = txn.Write(
68✔
460
                                artifact_provides_key, common::ByteVectorFromString(artifact_provides_str));
68✔
461
                        if (err != error::NoError) {
68✔
462
                                return err;
×
463
                        }
464
                }
465
                return txn_func(txn);
94✔
466
        });
188✔
467
}
468

469
expected::ExpectedBool MenderContext::MatchesArtifactDepends(const artifact::HeaderView &hdr_view) {
111✔
470
        auto ex_compatible_type = GetCompatibleType(hdr_view.type_info.type);
111✔
471
        if (!ex_compatible_type) {
111✔
472
                return expected::unexpected(ex_compatible_type.error());
4✔
473
        }
474
        string compatible_type = ex_compatible_type.value();
109✔
475

476
        auto ex_provides = LoadProvides();
109✔
477
        if (!ex_provides) {
109✔
478
                return expected::unexpected(ex_provides.error());
×
479
        }
480
        auto &provides = ex_provides.value();
109✔
481
        return ArtifactMatchesContext(provides, compatible_type, hdr_view);
109✔
482
}
483

484
expected::ExpectedBool ArtifactMatchesContext(
124✔
485
        const ProvidesData &provides,
486
        const string &compatible_type,
487
        const artifact::HeaderView &hdr_view) {
488
        using common::MapContainsStringKey;
489
        if (!MapContainsStringKey(provides, "artifact_name")) {
248✔
490
                return expected::unexpected(
×
491
                        MakeError(ValueError, "Missing artifact_name value in provides"));
×
492
        }
493

494
        auto hdr_depends = hdr_view.GetDepends();
124✔
495
        AssertOrReturnUnexpected(hdr_depends["device_type"].size() > 0);
250✔
496
        if (!common::VectorContainsString(hdr_depends["device_type"], compatible_type)) {
369✔
497
                log::Error("Artifact device type doesn't match");
4✔
498
                return false;
499
        }
500
        hdr_depends.erase("device_type");
242✔
501

502
        AssertOrReturnUnexpected(
155✔
503
                !MapContainsStringKey(hdr_depends, "artifact_name")
504
                || (hdr_depends["artifact_name"].size() > 0));
505

506
        AssertOrReturnUnexpected(
151✔
507
                !MapContainsStringKey(hdr_depends, "artifact_group")
508
                || (hdr_depends["artifact_group"].size() > 0));
509

510
        for (auto it : hdr_depends) {
141✔
511
                if (!common::MapContainsStringKey(provides, it.first)) {
17✔
512
                        log::Error("Missing '" + it.first + "' in provides, required by artifact depends");
4✔
513
                        return false;
514
                }
515
                if (!common::VectorContainsString(hdr_depends[it.first], provides.at(it.first))) {
15✔
516
                        log::Error(
4✔
517
                                "Provides value '" + provides.at(it.first) + "' doesn't match any of the '"
8✔
518
                                + it.first + "' artifact depends ("
8✔
519
                                + common::StringVectorToString(hdr_depends[it.first]) + ")");
20✔
520
                        return false;
521
                }
522
        }
523

524
        return true;
525
}
526

527
} // namespace context
528
} // namespace update
529
} // 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