@andrian.yablonskyy/thub-coordinator 1.1.16 → 1.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +2 -2
- package/public/js/client-config.js +10 -1
- package/scripts/install-systemd-unit.js +4 -12
- package/src/api/agent.js +15 -1
- package/src/api/resource.js +40 -0
- package/src/services/jobs.js +44 -1
- package/src/services/registry.js +15 -1
- package/src/web/routes.js +8 -4
- package/systemd/thub-coordinator.service +5 -3
- package/test/scheduler.test.js +2 -2
- package/test/updates.test.js +20 -4
- package/test/usb-power.test.js +129 -0
- package/test/views.test.js +25 -1
- package/views/help/_agent-cli.pug +13 -0
- package/views/help/_ci.pug +198 -9
- package/views/help/_client-machines.pug +3 -0
- package/views/help/_client-setup.pug +2 -0
- package/views/help/_coordinator.pug +8 -9
- package/views/help/_docker.pug +29 -0
- package/views/help/_env.pug +3 -0
- package/views/help/_git.pug +24 -11
- package/views/help/_kubernetes.pug +3 -3
- package/views/help/_lab-devices.pug +154 -0
- package/views/help/_storage.pug +218 -0
- package/views/help/_troubleshooting.pug +8 -0
- package/views/help/_usb-power.pug +147 -0
- package/views/help/index.pug +7 -1
- package/views/layout.pug +7 -4
- package/views/mixins/client-config.pug +3 -0
package/views/help/_ci.pug
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
+section('ci', 'CI/CD integration (GitHub
|
|
1
|
+
+section('ci', 'CI/CD integration (GitHub, GitLab, Bitbucket, Jenkins)', 'diagram-3')
|
|
2
2
|
p.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Any CI system can run TestHub tests. A pipeline step installs the Agent and runs #[code thub run … --wait]. The step's
|
|
4
|
+
exit code is the verdict, so the pipeline fails exactly when the tests do.
|
|
5
|
+
|
|
6
|
+
h3.h6 What every pipeline needs
|
|
7
|
+
ul.small
|
|
8
|
+
li
|
|
9
|
+
| #[strong A CI token]: create one on #[a(href="/admin/agents") CI tokens] (admins and maintainers). Store it as a masked or
|
|
10
|
+
| secret variable #[code THUB_KEY], and set #[code THUB_URL] to #[code #{coordinatorUrl}]. Its jobs get #[code A-] ids and the token's group.
|
|
11
|
+
li
|
|
12
|
+
| #[strong Node.js 24+ and outbound HTTPS to the Coordinator] on the CI runner. It needs no access to the lab. Use
|
|
13
|
+
| #[code npx -y @andrian.yablonskyy/thub-agent …] (pin a version, e.g. #[code @1.1.5], for reproducible pipelines) and set
|
|
14
|
+
| #[code THUB_NO_SELF_UPDATE=1] on short-lived runners.
|
|
15
|
+
li
|
|
16
|
+
| #[strong Exit codes] (#[code --wait]): #[code 0] PASSED, #[code 1] FAILED, #[code 2] ERROR/TIMEOUT/LOST, #[code 3] CANCELED,
|
|
17
|
+
| #[code 4] usage, auth or connection error.
|
|
18
|
+
li
|
|
19
|
+
| #[strong Canceling the pipeline cancels the job.] With #[code --wait], #[code SIGINT] and #[code SIGTERM] (how GitHub,
|
|
20
|
+
| GitLab and Jenkins stop a step) make the Agent cancel the TestHub job before exiting. If the runner kills it outright
|
|
21
|
+
| (#[code SIGKILL]), the job runs until its #[code --timeout], so keep that tight, or use #[code thub cancel].
|
|
22
|
+
li #[strong Timeouts]: make the CI step's timeout longer than the queue wait plus #[code --timeout].
|
|
23
|
+
li #[strong Secrets] go to the job with #[code --env NAME] (the value comes from the step's environment). The CI token never reaches the Client.
|
|
24
|
+
li #[strong Traceability]: #[code --meta] attaches the pipeline URL and commit to the job.
|
|
25
|
+
li
|
|
26
|
+
| #[strong Test reports]: the Coordinator keeps only the JUnit counts. For the CI system's own report view, have the command
|
|
27
|
+
| publish its JUnit XML to #[a(href="#storage") artifact storage], then download it in the CI step (examples below).
|
|
28
|
+
|
|
29
|
+
h3.h6 GitHub Actions
|
|
30
|
+
p.small.
|
|
31
|
+
Store the CI token as the repository secret #[code THUB_CI_TOKEN]. The test job runs on an ordinary hosted runner.
|
|
6
32
|
+code('.github/workflows/firmware.yml').
|
|
7
33
|
name: firmware
|
|
8
34
|
on: [push, pull_request]
|
|
@@ -38,8 +64,171 @@
|
|
|
38
64
|
st-flash --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/hw-tests.sh' \
|
|
39
65
|
--suite smoke --timeout 30m --wait \
|
|
40
66
|
--meta runUrl="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
67
|
+
p.small.
|
|
68
|
+
If the workflow is canceled, the runner sends #[code SIGINT] and the Agent cancels the job.
|
|
69
|
+
|
|
70
|
+
h3.h6 GitLab CI/CD
|
|
71
|
+
p.small.
|
|
72
|
+
Add #[code THUB_KEY] (masked; protected if only protected branches should reach hardware) and #[code ART_TOKEN] under
|
|
73
|
+
#[strong Settings → CI/CD → Variables]. The tests are cloned with the job's own #[code CI_JOB_TOKEN]; allow it under
|
|
74
|
+
#[strong Settings → CI/CD → Job token permissions]. The build hands the image URL to the test job as a #[code dotenv] report,
|
|
75
|
+
and the JUnit XML comes back through storage for GitLab's test report.
|
|
76
|
+
+code('.gitlab-ci.yml').
|
|
77
|
+
# .gitlab-ci.yml
|
|
78
|
+
stages: [build, test]
|
|
79
|
+
|
|
80
|
+
variables:
|
|
81
|
+
THUB_URL: #{coordinatorUrl}
|
|
82
|
+
THUB_NO_SELF_UPDATE: "1"
|
|
83
|
+
ART_BASE: https://artifactory.example.com/fw-local/app
|
|
84
|
+
|
|
85
|
+
build:
|
|
86
|
+
stage: build
|
|
87
|
+
image: registry.example.com/toolchains/arm-gcc:13
|
|
88
|
+
script:
|
|
89
|
+
- cmake -B build -G Ninja && cmake --build build
|
|
90
|
+
- URL="$ART_BASE/$CI_COMMIT_SHORT_SHA-$CI_PIPELINE_IID/app.bin"
|
|
91
|
+
- 'curl -fsS -H "Authorization: Bearer $ART_TOKEN" -T build/app.bin "$URL"'
|
|
92
|
+
- echo "IMAGE_URL=$URL" > build.env
|
|
93
|
+
artifacts:
|
|
94
|
+
reports:
|
|
95
|
+
dotenv: build.env
|
|
96
|
+
|
|
97
|
+
test-hw:
|
|
98
|
+
stage: test
|
|
99
|
+
image: node:24
|
|
100
|
+
needs: [build]
|
|
101
|
+
timeout: 1h # > queue wait + --timeout
|
|
102
|
+
script:
|
|
103
|
+
- npm i -g @andrian.yablonskyy/thub-agent
|
|
104
|
+
- |
|
|
105
|
+
thub run --type hw --label board:nucleo-f401re \
|
|
106
|
+
--download-file "$IMAGE_URL" \
|
|
107
|
+
--env CI_JOB_TOKEN --env CI_SERVER_HOST --env CI_PROJECT_PATH --env CI_COMMIT_SHA --env CI_JOB_ID --env ART_TOKEN \
|
|
108
|
+
--command 'git init -q src && cd src &&
|
|
109
|
+
git fetch -q --depth 1 "https://gitlab-ci-token:$CI_JOB_TOKEN@$CI_SERVER_HOST/$CI_PROJECT_PATH.git" "$CI_COMMIT_SHA" &&
|
|
110
|
+
git checkout -q FETCH_HEAD &&
|
|
111
|
+
st-flash --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/hw-tests.sh; rc=$?
|
|
112
|
+
for f in results/*.xml; do
|
|
113
|
+
curl -fsS -H "Authorization: Bearer $ART_TOKEN" -T "$f" "https://artifactory.example.com/qa/gitlab/$CI_JOB_ID/$(basename "$f")"
|
|
114
|
+
done
|
|
115
|
+
exit $rc' \
|
|
116
|
+
--timeout 30m --wait \
|
|
117
|
+
--meta pipelineUrl="$CI_PIPELINE_URL" --meta commit="$CI_COMMIT_SHA" || rc=$?
|
|
118
|
+
mkdir -p test-results
|
|
119
|
+
curl -fsS -H "Authorization: Bearer $ART_TOKEN" -o test-results/junit.xml \
|
|
120
|
+
"https://artifactory.example.com/qa/gitlab/$CI_JOB_ID/junit.xml" || true
|
|
121
|
+
exit ${rc:-0}
|
|
122
|
+
artifacts:
|
|
123
|
+
when: always
|
|
124
|
+
reports:
|
|
125
|
+
junit: test-results/*.xml
|
|
126
|
+
|
|
127
|
+
h3.h6 Bitbucket Pipelines
|
|
128
|
+
p.small.
|
|
129
|
+
Add #[code THUB_URL], #[code THUB_KEY] (secured), #[code ART_TOKEN] (secured) and #[code TESTS_TOKEN] under
|
|
130
|
+
#[strong Repository settings → Repository variables]. #[code TESTS_TOKEN] is a repository access token with read
|
|
131
|
+
access, used as the password of the user #[code x-token-auth]. #[code >-] folds the command into one line, so separate its
|
|
132
|
+
script with #[code ;] and #[code &&]. #[code after-script] collects the JUnit XML into #[code test-results/], which
|
|
133
|
+
Bitbucket reads automatically.
|
|
134
|
+
+code('bitbucket-pipelines.yml').
|
|
135
|
+
# bitbucket-pipelines.yml
|
|
136
|
+
image: node:24
|
|
137
|
+
|
|
138
|
+
definitions:
|
|
139
|
+
steps:
|
|
140
|
+
- step: &build
|
|
141
|
+
name: Build
|
|
142
|
+
image: registry.example.com/toolchains/arm-gcc:13
|
|
143
|
+
script:
|
|
144
|
+
- cmake -B build -G Ninja && cmake --build build
|
|
145
|
+
- URL="https://artifactory.example.com/fw-local/app/${BITBUCKET_COMMIT:0:7}-$BITBUCKET_BUILD_NUMBER/app.bin"
|
|
146
|
+
- 'curl -fsS -H "Authorization: Bearer $ART_TOKEN" -T build/app.bin "$URL"'
|
|
147
|
+
- echo "$URL" > image_url.txt
|
|
148
|
+
artifacts: [image_url.txt]
|
|
149
|
+
- step: &test-hw
|
|
150
|
+
name: HW tests
|
|
151
|
+
max-time: 60 # minutes; > queue wait + --timeout
|
|
152
|
+
script:
|
|
153
|
+
- export THUB_NO_SELF_UPDATE=1 IMAGE_URL="$(cat image_url.txt)"
|
|
154
|
+
- npm i -g @andrian.yablonskyy/thub-agent
|
|
155
|
+
- >-
|
|
156
|
+
thub run --type hw --label board:nucleo-f401re
|
|
157
|
+
--download-file "$IMAGE_URL"
|
|
158
|
+
--env TESTS_TOKEN --env BITBUCKET_REPO_FULL_NAME --env BITBUCKET_COMMIT --env BITBUCKET_BUILD_NUMBER --env ART_TOKEN
|
|
159
|
+
--command 'git init -q src && cd src &&
|
|
160
|
+
git fetch -q --depth 1 "https://x-token-auth:$TESTS_TOKEN@bitbucket.org/$BITBUCKET_REPO_FULL_NAME.git" "$BITBUCKET_COMMIT" &&
|
|
161
|
+
git checkout -q FETCH_HEAD &&
|
|
162
|
+
st-flash --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/hw-tests.sh; rc=$?;
|
|
163
|
+
curl -fsS -H "Authorization: Bearer $ART_TOKEN" -T results/junit.xml
|
|
164
|
+
"https://artifactory.example.com/qa/bitbucket/$BITBUCKET_BUILD_NUMBER/junit.xml"; exit $rc'
|
|
165
|
+
--timeout 30m --wait
|
|
166
|
+
--meta buildUrl="$BITBUCKET_GIT_HTTP_ORIGIN/pipelines/results/$BITBUCKET_BUILD_NUMBER"
|
|
167
|
+
after-script:
|
|
168
|
+
- mkdir -p test-results
|
|
169
|
+
- 'curl -fsS -H "Authorization: Bearer $ART_TOKEN" -o test-results/junit.xml "https://artifactory.example.com/qa/bitbucket/$BITBUCKET_BUILD_NUMBER/junit.xml" || true'
|
|
170
|
+
|
|
171
|
+
pipelines:
|
|
172
|
+
default:
|
|
173
|
+
- step: *build
|
|
174
|
+
- step: *test-hw
|
|
175
|
+
|
|
176
|
+
h3.h6 Jenkins
|
|
177
|
+
p.small.
|
|
178
|
+
Add the CI token as a #[strong Secret text] credential (#[code thub-ci-token]), plus #[code artifactory-token] and a
|
|
179
|
+
username/password for the tests repository (#[code tests-repo]). Run the stage in #[code node:24], or install Node.js with the
|
|
180
|
+
NodeJS plugin. #[code sh '''…'''] (single quotes) leaves every #[code $] to the shell, so no secret is interpolated by
|
|
181
|
+
Groovy. Aborting the build sends #[code SIGTERM], and the Agent cancels the job. In a freestyle job, put the same
|
|
182
|
+
#[code npx … run … --wait] line in an #[em Execute shell] step.
|
|
183
|
+
+code('Jenkinsfile').
|
|
184
|
+
// Jenkinsfile (declarative)
|
|
185
|
+
pipeline {
|
|
186
|
+
agent none
|
|
187
|
+
options { timeout(time: 1, unit: 'HOURS') } // > queue wait + --timeout
|
|
188
|
+
environment {
|
|
189
|
+
THUB_URL = '#{coordinatorUrl}'
|
|
190
|
+
THUB_NO_SELF_UPDATE = '1'
|
|
191
|
+
}
|
|
192
|
+
stages {
|
|
193
|
+
stage('Build') {
|
|
194
|
+
agent { label 'fw-build' }
|
|
195
|
+
environment { ART_TOKEN = credentials('artifactory-token') }
|
|
196
|
+
steps {
|
|
197
|
+
sh 'cmake -B build -G Ninja && cmake --build build'
|
|
198
|
+
script {
|
|
199
|
+
env.IMAGE_URL = "https://artifactory.example.com/fw-local/app/${env.GIT_COMMIT.take(7)}-${env.BUILD_NUMBER}/app.bin"
|
|
200
|
+
}
|
|
201
|
+
sh 'curl -fsS -H "Authorization: Bearer $ART_TOKEN" -T build/app.bin "$IMAGE_URL"'
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
stage('HW tests') {
|
|
205
|
+
agent { docker { image 'node:24' } }
|
|
206
|
+
environment {
|
|
207
|
+
THUB_KEY = credentials('thub-ci-token')
|
|
208
|
+
ART_TOKEN = credentials('artifactory-token')
|
|
209
|
+
NPM_CONFIG_CACHE = "${env.WORKSPACE}/.npm"
|
|
210
|
+
}
|
|
211
|
+
steps {
|
|
212
|
+
withCredentials([usernamePassword(credentialsId: 'tests-repo', usernameVariable: 'GIT_USER', passwordVariable: 'GIT_PASS')]) {
|
|
213
|
+
sh '''
|
|
214
|
+
npx -y @andrian.yablonskyy/thub-agent@1.1.5 run --type hw --label board:nucleo-f401re \
|
|
215
|
+
--download-file "$IMAGE_URL" \
|
|
216
|
+
--env GIT_USER --env GIT_PASS --env TESTS_COMMIT="$GIT_COMMIT" --env BUILD_TAG --env ART_TOKEN \
|
|
217
|
+
--command 'git clone -q "https://$GIT_USER:$GIT_PASS@git.example.com/fw/firmware-tests.git" src && cd src &&
|
|
218
|
+
git checkout -q "$TESTS_COMMIT" &&
|
|
219
|
+
st-flash --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/hw-tests.sh; rc=$?
|
|
220
|
+
curl -fsS -H "Authorization: Bearer $ART_TOKEN" -T results/junit.xml "https://artifactory.example.com/qa/jenkins/$BUILD_TAG/junit.xml"
|
|
221
|
+
exit $rc' \
|
|
222
|
+
--timeout 30m --wait --meta buildUrl="$BUILD_URL"
|
|
223
|
+
'''
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
post {
|
|
227
|
+
always {
|
|
228
|
+
sh 'mkdir -p test-results && curl -fsS -H "Authorization: Bearer $ART_TOKEN" -o test-results/junit.xml "https://artifactory.example.com/qa/jenkins/$BUILD_TAG/junit.xml" || true'
|
|
229
|
+
junit allowEmptyResults: true, testResults: 'test-results/*.xml'
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
li A physical machine (a mini-PC, NUC or Raspberry-class ARM64 board) next to the bench.
|
|
17
17
|
li USB for the DUT's debugger (ST-Link), USB-serial adapters (UART) and DUT USB. A #[strong powered USB hub] is recommended.
|
|
18
18
|
li #[code stlink-tools] and/or #[code openocd], #[code usbutils] (for the dashboard's USB scan).
|
|
19
|
+
li To power-cycle boards: a hub with per-port power switching and #[code uhubctl] (#[a(href="#usb-power") USB port power]).
|
|
19
20
|
li Docker only if jobs run containers in their #[code --command].
|
|
20
21
|
li One Client instance per board. One machine can drive several boards (up to 8 ST-Links, UARTs and USB devices per instance).
|
|
21
22
|
.col-md-6
|
|
@@ -85,3 +86,5 @@
|
|
|
85
86
|
li Whatever tools your jobs' commands use (git, docker, a flasher) are installed. Their credentials come with each job as #[code --env] (#[a(href="#git") Using git], #[a(href="#docker") Using Docker]), not from the host.
|
|
86
87
|
li Under systemd, the service can only write to its state directory. Jobs write to #[code $THUB_WORK_DIR], not to #[code ~].
|
|
87
88
|
li Optional: a nightly reboot from the runner card's #[strong Reboot] tab (cron, host-local time). It waits for running jobs.
|
|
89
|
+
li Optional (HW): boards on switchable USB ports, so jobs and their owners can power-cycle them (#[a(href="#usb-power") USB port power]).
|
|
90
|
+
li Optional: boards on a smart socket or PDU outlet that jobs switch themselves. Install the tools they call (#[code curl], #[code snmp], …) and give each instance its device (#[a(href="#lab-devices") Smart sockets, PDUs, devices]).
|
|
@@ -124,3 +124,5 @@
|
|
|
124
124
|
thub-client restart
|
|
125
125
|
thub-client --config ~/.config/thub/dut1.json status # a specific instance
|
|
126
126
|
thub-client udev --print # the udev rules this config generates
|
|
127
|
+
thub-client power status # USB port power (hw-devices.usbPower)
|
|
128
|
+
thub-client power reset --port 2 --delay 3 # power-cycle one board, 3 s off
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
#[code ~/.config/thub/coordinator.json] (with a random #[code sessionSecret]) and
|
|
14
14
|
#[code ~/var/lib/thub] (database, avatars). It installs, enables and starts
|
|
15
15
|
#[code thub-coordinator.service], plus the root helper behind the navbar's #[strong Update app] button
|
|
16
|
-
(
|
|
16
|
+
(the unit starts the Coordinator with #[code --self-update]; see Updates below).
|
|
17
17
|
The unit uses #[code ProtectSystem=strict] with #[code PrivateTmp=yes], so the data directory and a
|
|
18
18
|
private #[code /tmp] (SQLite's temp files) are the only places the service can write.
|
|
19
19
|
|
|
@@ -176,11 +176,10 @@
|
|
|
176
176
|
h3.h6 Updates
|
|
177
177
|
p.small.mb-0.
|
|
178
178
|
The Coordinator checks npm for new Coordinator, Agent and Client versions. Admins update Agents and Clients
|
|
179
|
-
from the #[a(href="/admin/agents")
|
|
180
|
-
only between jobs.
|
|
181
|
-
|
|
182
|
-
#[code sudo
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
#[code thub self-update] and #[code thub-client self-update].
|
|
179
|
+
from the #[a(href="/admin/agents") CI tokens] and #[a(href="/runners") Runners] pages (always there); Clients install
|
|
180
|
+
an update only between jobs. The #[strong Coordinator]'s own update (the navbar's check for updates and
|
|
181
|
+
#[strong Update app]) is there only when it runs with #[code --self-update]. A systemd install has it on
|
|
182
|
+
(#[code sudo npm i -g] writes it into the unit); to turn it off, override #[code ExecStart] in a drop-in
|
|
183
|
+
(#[code sudo systemctl edit thub-coordinator]). Started any other way, e.g. in a container, it's off and the dashboard
|
|
184
|
+
shows nothing about Coordinator updates: update it by hand (#[code thub-admin self-update], #[code sudo npm i -g …])
|
|
185
|
+
or with a new image. Manual alternatives for the others: #[code thub self-update] and #[code thub-client self-update].
|
package/views/help/_docker.pug
CHANGED
|
@@ -41,6 +41,35 @@
|
|
|
41
41
|
td: code --device /dev/thub/dut1-uart
|
|
42
42
|
td HW: gives the container the DUT's UART (#[code /dev/thub/dut1-uart]). Use #[code --privileged] only if you must.
|
|
43
43
|
|
|
44
|
+
h3.h6 Registry logins, per registry
|
|
45
|
+
p.small.
|
|
46
|
+
Always #[code docker login <registry> -u <user> --password-stdin], with the secret piped in from #[code --env]
|
|
47
|
+
(never #[code -p]). Only the user and password differ. For cloud registries, mint the short-lived token in the CI step
|
|
48
|
+
and pass only that.
|
|
49
|
+
.table-responsive
|
|
50
|
+
table.table.table-sm.small.align-middle
|
|
51
|
+
thead
|
|
52
|
+
tr
|
|
53
|
+
th Registry
|
|
54
|
+
th User
|
|
55
|
+
th Password / token
|
|
56
|
+
tbody
|
|
57
|
+
each row in [['Your own (registry:2, Harbor)', 'a robot or service account', 'its password or token'], ['JFrog Artifactory', 'a service user', 'an access token (see Artifact storage)'], ['GitHub (ghcr.io)', 'any GitHub user name', 'a PAT with read:packages, or the workflow\'s GITHUB_TOKEN'], ['GitLab', 'gitlab-ci-token', 'the job\'s CI_JOB_TOKEN (or a deploy token)'], ['Docker Hub', 'your user', 'a personal access token'], ['AWS ECR', 'AWS', 'aws ecr get-login-password (12 h)'], ['Google Artifact Registry', 'oauth2accesstoken', 'gcloud auth print-access-token (1 h)'], ['Azure Container Registry', 'a service principal or token name', 'its secret or token password']]
|
|
58
|
+
tr
|
|
59
|
+
td= row[0]
|
|
60
|
+
td: code= row[1]
|
|
61
|
+
td= row[2]
|
|
62
|
+
+code('AWS ECR: token minted in the CI step, never AWS credentials in the job').
|
|
63
|
+
# CI step with AWS access (e.g. OIDC → an IAM role that can pull from ECR)
|
|
64
|
+
export ECR_PASSWORD=$(aws ecr get-login-password --region eu-central-1)
|
|
65
|
+
thub run --type sw --env ECR_PASSWORD --env REG=123456789012.dkr.ecr.eu-central-1.amazonaws.com \
|
|
66
|
+
--command 'echo "$ECR_PASSWORD" | docker login "$REG" -u AWS --password-stdin &&
|
|
67
|
+
docker run --rm --user "$(id -u):$(id -g)" -v "$THUB_WORK_DIR/src:/work" -w /work "$REG/test-runner:1.4" ./run-tests.sh' \
|
|
68
|
+
--wait
|
|
69
|
+
p.small.
|
|
70
|
+
A registry with a private-CA certificate needs #[code /etc/docker/certs.d/<registry>[:port]/ca.crt] on the Client host
|
|
71
|
+
(plus #[code client.cert] / #[code client.key] there if it asks for a client certificate). Never use #[code insecure-registries].
|
|
72
|
+
|
|
44
73
|
h3.h6 Clone and test inside a container, with a deploy key and a registry login from the job
|
|
45
74
|
p.small.
|
|
46
75
|
Everything comes from the Agent as #[code --env]: the registry and its credentials, and the private key the
|
package/views/help/_env.pug
CHANGED
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
['JOB_ARG / JOB_ARG_<n>', '--arg', 'all, space-separated / each (also "$@")'],
|
|
40
40
|
['JOB_TIMEOUT', '--timeout', 'seconds'],
|
|
41
41
|
['JOB_PRIORITY', '--priority', '0–100'],
|
|
42
|
+
['JOB_POWER_ON_START', '--power-on-start', 'on, off or reset (HW, when given)'],
|
|
43
|
+
['JOB_POWER_ON_END', '--power-on-end', 'on, off or reset (HW, when given)'],
|
|
44
|
+
['JOB_POWER_RESET_DELAY', '--power-reset-delay', 'seconds (when given)'],
|
|
42
45
|
['JOB_META_<KEY>', '--meta key=value', 'the value'],
|
|
43
46
|
['<NAME>', '--env NAME=value', 'your own variables, under the names you chose']
|
|
44
47
|
]
|
package/views/help/_git.pug
CHANGED
|
@@ -18,19 +18,32 @@
|
|
|
18
18
|
The token appears only inside the command's environment. To keep it out of #[code .git/config] as well, pass it as a header:
|
|
19
19
|
#[code git -c http.extraHeader="Authorization: Bearer $GH_TOKEN" clone …].
|
|
20
20
|
|
|
21
|
-
h3.h6 SSH with a key
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
h3.h6 SSH with a deploy key and a pinned host key
|
|
22
|
+
p.small.
|
|
23
|
+
Create a key pair for TestHub only, without a passphrase (#[code ssh-keygen -t ed25519 -N '' -f thub_deploy]), and add the
|
|
24
|
+
#[strong public] key as a read-only deploy key: GitHub, Settings → Deploy keys; GitLab, Settings → Repository → Deploy keys;
|
|
25
|
+
Bitbucket, Repository settings → Access keys. The private key goes with the job as #[code --env]. Pin the server's
|
|
26
|
+
host key too: fetch it once with #[code ssh-keyscan], check it against the fingerprints your git host publishes,
|
|
27
|
+
and store both in your CI's secret store.
|
|
28
|
+
+code('Deploy key and known_hosts from --env, used for this job only').
|
|
29
|
+
# once, then store both in your CI's secret store:
|
|
30
|
+
ssh-keyscan github.com > known_hosts # GitLab: gitlab.com; Bitbucket: bitbucket.org; your server: -p <port> host
|
|
31
|
+
export GIT_KEY="$(cat thub_deploy)" GIT_KNOWN_HOSTS="$(cat known_hosts)"
|
|
32
|
+
|
|
33
|
+
thub run --type hw --label board:nucleo-f401re --env GIT_KEY --env GIT_KNOWN_HOSTS \
|
|
34
|
+
--command 'umask 077 &&
|
|
35
|
+
printf "%s\n" "$GIT_KEY" > "$THUB_WORK_DIR/id" && printf "%s\n" "$GIT_KNOWN_HOSTS" > "$THUB_WORK_DIR/known_hosts" &&
|
|
36
|
+
export GIT_SSH_COMMAND="ssh -i $THUB_WORK_DIR/id -o IdentitiesOnly=yes -o UserKnownHostsFile=$THUB_WORK_DIR/known_hosts -o StrictHostKeyChecking=yes" &&
|
|
37
|
+
git clone --depth 1 --recurse-submodules --shallow-submodules git@github.com:yourorg/firmware-tests.git src && cd src &&
|
|
29
38
|
./ci/test.sh' \
|
|
30
39
|
--wait
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
ul.small
|
|
41
|
+
li #[code GIT_SSH_COMMAND] is exported, so #[code git fetch] and submodules on the same host use it as well.
|
|
42
|
+
li The key and #[code known_hosts] are written with #[code umask 077] into the job's directory and deleted with it.
|
|
43
|
+
li #[code StrictHostKeyChecking=yes] refuses an unknown host. #[code accept-new] protects nothing here: every job starts with an empty #[code known_hosts].
|
|
44
|
+
li Another port (Bitbucket Server, Gitea): #[code ssh://git@git.example.com:7999/proj/repo.git] and #[code ssh-keyscan -p 7999 git.example.com].
|
|
45
|
+
li To keep the key off the disk, load it into #[code ssh-agent]: #[code eval "$(ssh-agent -s)" && printf "%s\n" "$GIT_KEY" | ssh-add -].
|
|
46
|
+
li A key in the service user's #[code ~/.ssh] needs no #[code --env], but every job on that host can then use it.
|
|
34
47
|
|
|
35
48
|
p.small.
|
|
36
49
|
To clone #[em inside a container] with a key passed the same way (held by #[code ssh-agent], never written to disk), see
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
p.
|
|
3
3
|
The Coordinator runs as one container: the published package in a Node.js image, its SQLite database on a
|
|
4
4
|
persistent volume, TLS at the ingress. Clients and Agents connect from outside at #[code publicUrl].
|
|
5
|
-
Self-update stays off: the image starts the Coordinator without #[code --self-update],
|
|
6
|
-
deploying a new image tag.
|
|
5
|
+
Self-update stays off: the image starts the Coordinator without #[code --self-update], so the dashboard shows
|
|
6
|
+
nothing about Coordinator updates; you update it by deploying a new image tag.
|
|
7
7
|
|
|
8
8
|
h3.h6 1. Build the image
|
|
9
9
|
+code('Dockerfile').
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
h3.h6 Things to know
|
|
161
161
|
ul.small
|
|
162
162
|
li #[strong One replica.] One SQLite file and an in-process scheduler: no scale-out, no HPA. #[code Recreate] means a few seconds of downtime per upgrade; Clients and Agents reconnect by themselves.
|
|
163
|
-
li #[strong Updating:] deploy a new image tag. Migrations run on the new pod's first start.
|
|
163
|
+
li #[strong Updating:] deploy a new image tag. Migrations run on the new pod's first start. No #[code --self-update], so no Coordinator update check or #[strong Update app] on the dashboard. Agents and Clients still update from it.
|
|
164
164
|
li #[strong Storage:] block storage (ReadWriteOnce), not NFS. A separate database volume: mount it and set #[code THUB_DB_PATH].
|
|
165
165
|
li #[strong /tmp] is an #[code emptyDir]: SQLite needs a writable temp directory and the root filesystem is read-only.
|
|
166
166
|
li #[strong Backups:] #[code kubectl -n thub exec deploy/thub-coordinator -- node -e "new (require('/app/node_modules/better-sqlite3'))('/var/lib/thub/thub.db').backup('/var/lib/thub/backup.db')"], then #[code kubectl cp].
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
+section('lab-devices', 'Smart sockets, PDUs and other lab devices', 'plug')
|
|
2
|
+
p.
|
|
3
|
+
TestHub has one power control built in: #[a(href="#usb-power") USB port power] with uhubctl. Anything else in the lab
|
|
4
|
+
is driven by #[strong the job itself]: a smart socket (Shelly, Tasmota, TP-Link Kasa, or any socket Home Assistant
|
|
5
|
+
controls), a PDU outlet (APC, Raritan, …), a USB relay board, a bench supply. The job's #[code --command], or a script
|
|
6
|
+
from its repository, calls the device with its own tools. The Client needs no configuration for it.
|
|
7
|
+
|
|
8
|
+
.table-responsive
|
|
9
|
+
table.table.table-sm.small.align-middle
|
|
10
|
+
thead
|
|
11
|
+
tr
|
|
12
|
+
th(style="width: 18%")
|
|
13
|
+
th USB port power (built in)
|
|
14
|
+
th Socket, PDU or other device
|
|
15
|
+
tbody
|
|
16
|
+
tr
|
|
17
|
+
th Configured in
|
|
18
|
+
td The Client's #[code hw-devices.usbPower]
|
|
19
|
+
td The job's command or script, plus a variable naming the bench's device
|
|
20
|
+
tr
|
|
21
|
+
th Start / end of job
|
|
22
|
+
td #[code --power-on-start] / #[code --power-on-end]. The end action runs even on cancel
|
|
23
|
+
td The script's own steps; on cancel, only as reliable as its trap (below)
|
|
24
|
+
tr
|
|
25
|
+
th While the job runs
|
|
26
|
+
td #[code thub power reset <jobId>] (owner)
|
|
27
|
+
td Whenever the script decides. It can't be triggered from outside
|
|
28
|
+
tr
|
|
29
|
+
th Credentials
|
|
30
|
+
td None
|
|
31
|
+
td #[code --env], or the Client host's environment
|
|
32
|
+
|
|
33
|
+
h3.h6 Before you start
|
|
34
|
+
ul.small
|
|
35
|
+
li #[strong The command runs on the Client host]: the device must be reachable from there (the lab network), never through the Coordinator.
|
|
36
|
+
li #[strong The tools must be on the Client host]: #[code curl], #[code snmpset] (#[code sudo apt install snmp]), #[code kasa] (#[code pipx install python-kasa]), …
|
|
37
|
+
li #[strong Credentials come with the job] as #[code --env], so they're masked and dropped when the job ends.
|
|
38
|
+
li
|
|
39
|
+
| #[strong Tell each Client which device is its bench's.] Jobs inherit the Client service's environment, so add a
|
|
40
|
+
| systemd drop-in per instance. Every job on that Client can read it, so put addresses there, not secrets.
|
|
41
|
+
| Alternatively, pin jobs with #[code --client] and map #[code $JOB_CLIENT] in your script.
|
|
42
|
+
+code('Client host').
|
|
43
|
+
sudo systemctl edit thub-client@dut1
|
|
44
|
+
# [Service]
|
|
45
|
+
# Environment=BENCH_POWER=shelly:10.0.20.11
|
|
46
|
+
sudo systemctl restart thub-client@dut1
|
|
47
|
+
systemctl show thub-client@dut1 -p Environment # check
|
|
48
|
+
|
|
49
|
+
h3.h6 One command per device
|
|
50
|
+
p.small.
|
|
51
|
+
Each example switches the device off, waits, switches it on, then tests. API details vary between models and firmware,
|
|
52
|
+
so check your device's manual.
|
|
53
|
+
+code('Shelly Gen2/Plus/Pro (RPC; digest auth, user admin)').
|
|
54
|
+
thub run --type hw --label board:nucleo-f401re --env SHELLY_PASSWORD \
|
|
55
|
+
--command 'S="http://$BENCH_IP/rpc/Switch.Set?id=0"
|
|
56
|
+
curl -fsS --digest -u "admin:$SHELLY_PASSWORD" "$S&on=false" && sleep 2 &&
|
|
57
|
+
curl -fsS --digest -u "admin:$SHELLY_PASSWORD" "$S&on=true" && sleep 3 && ./ci/test.sh' --wait
|
|
58
|
+
# Gen1: http://$BENCH_IP/relay/0?turn=off|on (basic auth: curl -u user:password)
|
|
59
|
+
+code('Tasmota (Power2 … for multi-relay devices)').
|
|
60
|
+
thub run --type hw … --env TASMOTA_PASSWORD \
|
|
61
|
+
--command 'T="http://$BENCH_IP/cm?user=admin&password=$TASMOTA_PASSWORD&cmnd=Power%20"
|
|
62
|
+
curl -fsS "${T}Off" && sleep 2 && curl -fsS "${T}On" && sleep 3 && ./ci/test.sh'
|
|
63
|
+
+code('Home Assistant (any socket it controls; a long-lived access token)').
|
|
64
|
+
thub run --type hw … --env HA_URL=http://ha.lab:8123 --env HA_TOKEN \
|
|
65
|
+
--command 'ha() { curl -fsS -X POST -H "Authorization: Bearer $HA_TOKEN" -H "Content-Type: application/json" \
|
|
66
|
+
-d "{\"entity_id\":\"$BENCH_SWITCH\"}" "$HA_URL/api/services/switch/turn_$1"; }
|
|
67
|
+
ha off && sleep 2 && ha on && sleep 3 && ./ci/test.sh'
|
|
68
|
+
+code('TP-Link Kasa / Tapo (python-kasa; newer firmware needs --username/--password)').
|
|
69
|
+
thub run --type hw … --command 'kasa --host "$BENCH_IP" off && sleep 2 && kasa --host "$BENCH_IP" on && sleep 3 && ./ci/test.sh'
|
|
70
|
+
+code('APC switched PDU (SNMP: 1 on, 2 off, 3 reboot; older AP79xx: sPDUOutletCtl …1.4.4.2.1.3.<outlet>)').
|
|
71
|
+
thub run --type hw … --env PDU_COMMUNITY \
|
|
72
|
+
--command 'snmpset -v1 -c "$PDU_COMMUNITY" "$PDU_HOST" 1.3.6.1.4.1.318.1.1.12.3.3.1.1.4.$PDU_OUTLET i 3 && sleep 15 && ./ci/test.sh'
|
|
73
|
+
# state: snmpget -v1 -c "$PDU_COMMUNITY" "$PDU_HOST" 1.3.6.1.4.1.318.1.1.12.3.5.1.1.4.$PDU_OUTLET (1 on, 2 off)
|
|
74
|
+
p.small.
|
|
75
|
+
Anything else with a command line or a network interface works the same way: an SSH-managed PDU
|
|
76
|
+
(#[code ssh pdu1 "olOff 5"]), a USB relay board (#[code usbrelay]), a bench supply over SCPI
|
|
77
|
+
(#[code echo 'OUTP OFF' | nc psu1.lab 5025]).
|
|
78
|
+
|
|
79
|
+
h3.h6 A power script in the test repository
|
|
80
|
+
p.small.
|
|
81
|
+
Once several jobs need it, keep the device logic in the repository the job clones. A single script can cover every
|
|
82
|
+
kind of device in the lab, keyed by #[code $BENCH_POWER]:
|
|
83
|
+
+code('ci/power.sh').
|
|
84
|
+
#!/bin/sh
|
|
85
|
+
# ci/power.sh on|off|reset [off-seconds] — switch this bench's socket or PDU outlet.
|
|
86
|
+
# BENCH_POWER: shelly:10.0.20.11 | tasmota:10.0.20.12 | ha:switch.bench1 | apc:pdu1.lab:5
|
|
87
|
+
# Credentials (--env): POWER_PASSWORD (Shelly, Tasmota), HA_URL + HA_TOKEN, PDU_COMMUNITY.
|
|
88
|
+
set -eu
|
|
89
|
+
: "${BENCH_POWER:?BENCH_POWER is not set for this Client}"
|
|
90
|
+
kind=${BENCH_POWER%%:*} target=${BENCH_POWER#*:}
|
|
91
|
+
|
|
92
|
+
switch() { # $1: on | off
|
|
93
|
+
case $kind in
|
|
94
|
+
shelly)
|
|
95
|
+
url="http://$target/rpc/Switch.Set?id=0&on=$([ "$1" = on ] && echo true || echo false)"
|
|
96
|
+
if [ -n "${POWER_PASSWORD:-}" ]; then curl -fsS --digest -u "admin:$POWER_PASSWORD" "$url"; else curl -fsS "$url"; fi ;;
|
|
97
|
+
tasmota)
|
|
98
|
+
curl -fsS "http://$target/cm?user=admin&password=${POWER_PASSWORD:-}&cmnd=Power%20$1" ;;
|
|
99
|
+
ha)
|
|
100
|
+
curl -fsS -X POST -H "Authorization: Bearer $HA_TOKEN" -H 'Content-Type: application/json' \
|
|
101
|
+
-d "{\"entity_id\":\"$target\"}" "$HA_URL/api/services/switch/turn_$1" ;;
|
|
102
|
+
apc)
|
|
103
|
+
snmpset -v1 -c "$PDU_COMMUNITY" "${target%:*}" "1.3.6.1.4.1.318.1.1.12.3.3.1.1.4.${target##*:}" \
|
|
104
|
+
i "$([ "$1" = on ] && echo 1 || echo 2)" ;;
|
|
105
|
+
*)
|
|
106
|
+
echo "power.sh: unknown device kind '$kind' in BENCH_POWER" >&2; exit 2 ;;
|
|
107
|
+
esac >/dev/null
|
|
108
|
+
echo "power: $BENCH_POWER $1"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
case ${1:-} in
|
|
112
|
+
on|off) switch "$1" ;;
|
|
113
|
+
reset) switch off; sleep "${2:-1}"; switch on ;;
|
|
114
|
+
*) echo "usage: power.sh on|off|reset [off-seconds]" >&2; exit 2 ;;
|
|
115
|
+
esac
|
|
116
|
+
+code('Use it from the job').
|
|
117
|
+
thub run --type hw --label board:nucleo-f401re \
|
|
118
|
+
--download-file "$IMAGE_URL" --env GH_TOKEN --env POWER_PASSWORD \
|
|
119
|
+
--command 'git clone --depth 1 "https://x-access-token:$GH_TOKEN@github.com/yourorg/firmware-tests.git" src && cd src &&
|
|
120
|
+
ci/power.sh reset 2 && sleep 3 &&
|
|
121
|
+
st-flash --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh' --wait
|
|
122
|
+
+code('Or download the script instead of cloning a repository').
|
|
123
|
+
thub run --type hw … --download-file https://artifactory.example.com/lab/tools/power.sh \
|
|
124
|
+
--command 'sh "$THUB_DOWNLOAD_1" reset 2 && ./run-tests.sh'
|
|
125
|
+
|
|
126
|
+
h3.h6 Switching it off again, also when the job is canceled
|
|
127
|
+
p.small.
|
|
128
|
+
When a job is canceled, times out or its Client is stopped, the Client sends #[code SIGTERM] to the job's
|
|
129
|
+
#[strong shell only], then #[code SIGKILL] 10 s later. To power the board off whatever happens:
|
|
130
|
+
ul.small
|
|
131
|
+
li Start the wrapper with #[code exec], so the wrapper is the shell that receives the signal.
|
|
132
|
+
li Run the tests in the background and #[code wait] for them. A shell runs a trap only after its current foreground command ends.
|
|
133
|
+
li Finish the cleanup within 10 s. A #[code SIGKILL] can't be trapped.
|
|
134
|
+
+code('ci/run.sh — cold-boot, flash, test, always power off').
|
|
135
|
+
#!/bin/sh
|
|
136
|
+
set -u
|
|
137
|
+
cd "$(dirname "$0")/.."
|
|
138
|
+
test_pid=
|
|
139
|
+
off() { ci/power.sh off || echo "power: could not switch $BENCH_POWER off" >&2; }
|
|
140
|
+
trap '[ -n "$test_pid" ] && kill "$test_pid" 2>/dev/null; off; exit 143' TERM INT
|
|
141
|
+
|
|
142
|
+
ci/power.sh reset 2 || exit 2
|
|
143
|
+
sleep 3
|
|
144
|
+
st-flash --reset write "$THUB_DOWNLOAD_1" 0x08000000 || { off; exit 2; }
|
|
145
|
+
./ci/test.sh "$@" & test_pid=$!
|
|
146
|
+
wait "$test_pid"; rc=$?
|
|
147
|
+
off
|
|
148
|
+
exit "$rc"
|
|
149
|
+
+code('Started with exec').
|
|
150
|
+
thub run --type hw … --command 'git clone … src && cd src && exec ci/run.sh' --wait
|
|
151
|
+
+note.
|
|
152
|
+
For a guarantee independent of the job, also set a timer on the device as a dead-man switch: Shelly's
|
|
153
|
+
#[code toggle_after] (#[code Switch.Set?id=0&on=true&toggle_after=3600] switches it off again after an hour) or Tasmota's
|
|
154
|
+
#[code PulseTime]. A failing #[code power.sh] step fails the job, since the command's exit code is the verdict.
|