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

mendersoftware / mender / 968408678

pending completion
968408678

push

gitlab-ci

oleorhagen
feat(artifact/scripts): Add support for executing state scripts

Ticket: MEN-6636
Changelog: None

Signed-off-by: Ole Petter <ole.orhagen@northern.tech>

126 of 126 new or added lines in 4 files covered. (100.0%)

5405 of 6857 relevant lines covered (78.82%)

195.75 hits per line

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

83.33
/artifact/v3/scripts/executor.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 <artifact/v3/scripts/executor.hpp>
16

17
#include <algorithm>
18
#include <chrono>
19
#include <regex>
20
#include <string>
21

22
#include <common/common.hpp>
23
#include <common/expected.hpp>
24
#include <common/path.hpp>
25

26

27
namespace mender {
28
namespace artifact {
29
namespace scripts {
30
namespace executor {
31

32
namespace expected = mender::common::expected;
33

34

35
using expected::ExpectedBool;
36

37
namespace common = mender::common;
38
namespace merror = mender::common::error;
39
namespace path = mender::common::path;
40

41

42
const int expected_state_script_version {3};
43

44
unordered_map<const State, string> state_map {
45
        {State::Idle, "Idle"},
46
        {State::Sync, "Sync"},
47
        {State::Download, "Download"},
48
        {State::ArtifactInstall, "ArtifactInstall"},
49
        {State::ArtifactReboot, "ArtifactReboot"},
50
        {State::ArtifactCommit, "ArtifactCommit"},
51
        {State::ArtifactRollback, "ArtifactRollback"},
52
        {State::ArtifactRollbackReboot, "ArtifactRollbackReboot"},
53
        {State::ArtifactFailure, "ArtifactFailure"},
54
};
55

56
unordered_map<const Action, string> action_map {
57
        {Action::Enter, "Enter"},
58
        {Action::Leave, "Leave"},
59
        {Action::Error, "Error"},
60
};
61

62
merror::Error CorrectVersionFile(const string &path) {
6✔
63
        ifstream vf {path};
12✔
64

65
        // Missing file is OK
66
        if (!vf.is_open()) {
6✔
67
                return merror::NoError;
4✔
68
        }
69

70
        if (!vf) {
2✔
71
                auto errnum {errno};
×
72
                return merror::Error(
73
                        generic_category().default_error_condition(errnum), "Failed to open the version file");
×
74
        }
75

76
        string version;
4✔
77
        vf >> version;
2✔
78
        if (!vf) {
2✔
79
                auto errnum {errno};
×
80
                return merror::Error(
81
                        generic_category().default_error_condition(errnum),
×
82
                        "Error reading the version number from the version file");
×
83
        }
84

85
        auto exp_version_number {common::StringToLongLong(version)};
4✔
86
        if (!exp_version_number) {
2✔
87
                return executor::error::MakeError(
88
                        executor::error::VersionFileError,
89
                        "Error reading the version number from the version file. Is the file in the correct format? error: "
90
                                + exp_version_number.error().String());
2✔
91
        }
92

93
        if (exp_version_number.value() != expected_state_script_version) {
1✔
94
                return executor::error::MakeError(
95
                        executor::error::VersionFileWrongVersionError,
96
                        "Unexpected Artifact script version found: " + to_string(exp_version_number.value()));
×
97
        }
98
        return merror::NoError;
1✔
99
}
100

101
bool isValidStateScript(const string &file, State state, Action action) {
11✔
102
        string expression {
103
                "(" + state_map.at(state) + ")" + "_(" + action_map.at(action) + ")_[0-9][0-9](_\\S+)?"};
33✔
104
        log::Trace(
11✔
105
                "verifying the State script format of the file: " + file
11✔
106
                + " using the regular expression: " + expression);
22✔
107
        const regex artifact_script_regexp {expression, std::regex_constants::ECMAScript};
11✔
108
        return regex_match(path::BaseName(file), artifact_script_regexp);
22✔
109
}
110

111
function<bool(const string &)> Matcher(State state, Action action) {
7✔
112
        return [state, action](const string &file) {
22✔
113
                return path::IsExecutable(file) and isValidStateScript(file, state, action);
11✔
114
        };
7✔
115
}
116

117
bool isArtifactScript(State state) {
15✔
118
        switch (state) {
15✔
119
        case State::Idle:
4✔
120
        case State::Sync:
121
        case State::Download:
122
                return false;
4✔
123
        default:
11✔
124
                return true;
11✔
125
        }
126
}
127

128
string ScriptRunner::ScriptPath(State state) {
7✔
129
        if (isArtifactScript(state)) {
7✔
130
                return this->artifact_script_path_;
5✔
131
        }
132
        return this->rootfs_script_path_;
2✔
133
}
134

135
string ScriptRunner::Name() {
×
136
        return state_map.at(state_) + action_map.at(action_);
×
137
}
138

139
ScriptRunner::ScriptRunner(
8✔
140
        events::EventLoop &loop,
141
        State state,
142
        Action action,
143
        int state_script_timeout_seconds,
144
        const string &artifact_script_path,
145
        const string &rootfs_script_path,
146
        mender::common::processes::OutputCallback stdout_callback,
147
        mender::common::processes::OutputCallback stderr_callback) :
8✔
148
        loop_ {loop},
149
        is_artifact_script_ {isArtifactScript(state)},
8✔
150
        timeout_ {state_script_timeout_seconds},
151
        state_ {state},
152
        action_ {action},
153
        artifact_script_path_ {artifact_script_path},
154
        rootfs_script_path_ {rootfs_script_path},
155
        stdout_callback_ {stdout_callback},
156
        stderr_callback_ {stderr_callback},
157
        error_script_error_ {merror::NoError} {};
8✔
158

159
void ScriptRunner::HandleErrorScriptError(
2✔
160
        Error err,
161
        vector<string>::iterator current_script,
162
        vector<string>::iterator end,
163
        HandlerFunction handler) {
164
        // Collect the error and carry on
165
        if (err.code
2✔
166
                == common::processes::MakeError(common::processes::NonZeroExitStatusError, "").code) {
2✔
167
                this->error_script_error_ = this->error_script_error_.FollowedBy(executor::error::MakeError(
2✔
168
                        executor::error::NonZeroExitStatusError,
169
                        "Got non zero exit code from script: " + *current_script));
4✔
170
        } else {
171
                this->error_script_error_ = this->error_script_error_.FollowedBy(err);
×
172
        }
173

174
        // Schedule the next script execution
175
        auto local_err = Execute(std::next(current_script), end, handler);
2✔
176
        if (local_err != merror::NoError) {
2✔
177
                return handler(local_err);
×
178
        }
179
}
180

181
void ScriptRunner::HandleScriptError(Error err, HandlerFunction handler) {
1✔
182
        if (err.code
1✔
183
                == common::processes::MakeError(common::processes::NonZeroExitStatusError, "").code) {
1✔
184
                if (this->script_.get()->GetExitStatus() == 21) {
1✔
185
                        return handler(executor::error::MakeError(
×
186
                                executor::error::RetryExitCodeError, "Received exit code 21"));
×
187
                }
188
                return handler(executor::error::MakeError(
1✔
189
                        executor::error::NonZeroExitStatusError,
190
                        "Received error code: " + to_string(this->script_.get()->GetExitStatus())));
2✔
191
        }
192
        return handler(err);
×
193
}
194

195
Error ScriptRunner::Execute(
16✔
196
        vector<string>::iterator current_script,
197
        vector<string>::iterator end,
198
        HandlerFunction handler) {
199
        // No more scripts to execute
200
        if (current_script == end) {
16✔
201
                handler(this->error_script_error_); // Success
6✔
202
                return merror::NoError;
6✔
203
        }
204

205
        log::Info("Running Artifact script: " + *current_script);
10✔
206

207
        this->script_.reset(new mender::common::processes::Process({*current_script}));
20✔
208
        auto err {this->script_->Start(stdout_callback_, stderr_callback_)};
30✔
209
        if (err != merror::NoError) {
10✔
210
                return err;
×
211
        }
212

213
        return this->script_.get()->AsyncWait(
214
                this->loop_,
215
                [this, current_script, end, handler](Error err) {
23✔
216
                        if (err != merror::NoError) {
10✔
217
                                if (this->action_ == Action::Error) {
3✔
218
                                        return HandleErrorScriptError(err, current_script, end, handler);
3✔
219
                                }
220
                                return HandleScriptError(err, handler);
1✔
221
                        }
222
                        // Schedule the next script execution
223
                        auto local_err = Execute(std::next(current_script), end, handler);
7✔
224
                        if (local_err != merror::NoError) {
7✔
225
                                return handler(local_err);
×
226
                        }
227
                },
228
                chrono::seconds {this->timeout_});
10✔
229
}
230

231
Error ScriptRunner::AsyncRunScripts(HandlerFunction handler) {
8✔
232
        if (this->is_artifact_script_) {
8✔
233
                // Verify the version in the version file (OK if no version file present)
234
                auto version_file_error {
235
                        CorrectVersionFile(path::Join(this->artifact_script_path_, "version"))};
6✔
236
                if (version_file_error != merror::NoError) {
6✔
237
                        return version_file_error;
1✔
238
                }
239
        }
240

241
        // Collect
242
        auto exp_scripts {path::ListFiles(ScriptPath(state_), Matcher(state_, action_))};
21✔
243
        if (!exp_scripts) {
7✔
244
                return executor::error::MakeError(
245
                        executor::error::Code::CollectionError,
246
                        "Failed to get the scripts, error: " + exp_scripts.error().String());
×
247
        }
248
        auto scripts {exp_scripts.value()};
7✔
249

250
        // Sort
251
        sort(scripts.begin(), scripts.end());
7✔
252
        this->collected_scripts_ = scripts;
7✔
253

254
        // Execute
255
        auto scripts_iterator {this->collected_scripts_.begin()};
7✔
256
        auto scripts_iterator_end {this->collected_scripts_.end()};
7✔
257
        return Execute(scripts_iterator, scripts_iterator_end, handler);
7✔
258
}
259

260
} // namespace executor
261
} // namespace scripts
262
} // namespace artifact
263
} // 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