@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.
@@ -0,0 +1,218 @@
1
+ +section('storage', 'Artifact storage and certificates: Artifactory, S3, Google Drive, (S)FTP', 'cloud-arrow-up')
2
+ p.
3
+ TestHub stores no files. A job's #[strong inputs] come from wherever their URLs point, and its #[strong outputs]
4
+ (reports, binaries, core dumps) go wherever its command uploads them. The patterns are the same for every kind of storage:
5
+ .table-responsive
6
+ table.table.table-sm.small.align-middle
7
+ thead
8
+ tr
9
+ th(style="width: 22%") Direction
10
+ th How
11
+ th(style="width: 30%") Credentials
12
+ tbody
13
+ tr
14
+ td Input, anonymous HTTP(S)
15
+ td #[code --download-file <url>]: fetched by the Client before the command (#[code $THUB_DOWNLOAD_1] …)
16
+ td None. A plain GET with no headers
17
+ tr
18
+ td Input, signed URL
19
+ td #[code --download-file "<pre-signed URL>"] (S3, GCS, Azure SAS, Artifactory signed URLs)
20
+ td In the URL. It shows on the job page, so keep its expiry short
21
+ tr
22
+ td Input, private
23
+ td Fetched in #[code --command] (#[code curl], #[code aws], #[code rclone], …)
24
+ td #[code --env NAME]: masked, dropped when the job ends
25
+ tr
26
+ td Input, client certificate
27
+ td Fetched in #[code --command] with #[code curl --cert] (#[code --download-file] can't present one)
28
+ td The certificate and key as #[code --env]
29
+ tr
30
+ td Output
31
+ td Uploaded in #[code --command], then listed in #[code $THUB_ARTIFACTS_FILE] so it shows on the job page
32
+ td #[code --env NAME]
33
+ ul.small
34
+ li The tools run on the #[strong Client host], which must reach the storage. Install #[code awscli], #[code rclone], … there, or run them in a container.
35
+ li Keep secrets off the command line (visible in #[code ps]): #[code curl -K -] reads them from standard input, and #[code aws] and #[code rclone] read them from the environment.
36
+ li Artifact links must be #[code http(s)]: an #[code s3://] or #[code sftp://] link is dropped from the list.
37
+ li Keep the verdict: run the tests, save #[code rc=$?], upload, then #[code exit $rc].
38
+ li Name outputs after the job (#[code …/$THUB_JOB_ID/…]) or a CI id, so a CI step can find them.
39
+
40
+ h3.h6 Helper: list what was published
41
+ p.small The examples below call #[code ci/artifacts.sh] from the tests repository, and assume the command has cloned it.
42
+ +code('ci/artifacts.sh').
43
+ #!/bin/sh
44
+ # ci/artifacts.sh name link [file] — append one entry to $THUB_ARTIFACTS_FILE (§7.3)
45
+ name=$1 link=$2 file=${3:-}
46
+ size=null; [ -n "$file" ] && size=$(wc -c < "$file" | tr -d ' ')
47
+ entry=$(printf '{"name":"%s","size":%s,"link":"%s","timestamp":%s}' "$name" "$size" "$link" "$(date +%s)")
48
+ if [ -s "$THUB_ARTIFACTS_FILE" ]; then # plain shell, not sed: signed URLs contain & and |
49
+ list=$(cat "$THUB_ARTIFACTS_FILE")
50
+ printf '%s,%s]\n' "${list%]}" "$entry" > "$THUB_ARTIFACTS_FILE"
51
+ else
52
+ printf '[%s]\n' "$entry" > "$THUB_ARTIFACTS_FILE"
53
+ fi
54
+
55
+ h3.h6 HTTPS certificates: private CAs and client certificates
56
+ p.small.
57
+ #[strong A private CA.] #[code --download-file] is fetched by the Client's Node.js process, which doesn't read the
58
+ system CA store, so the download fails with #[code SELF_SIGNED_CERT_IN_CHAIN]. Give the Client the CA with
59
+ #[code NODE_EXTRA_CA_CERTS], once per host, in a drop-in for the template unit, which applies to every instance.
60
+ #[code update-ca-certificates] covers #[code curl] and #[code git] in jobs, and Docker reads
61
+ #[code /etc/docker/certs.d/<registry>/ca.crt]. Never switch verification off in a job.
62
+ +code('Client host').
63
+ sudo cp lab-ca.crt /usr/local/share/ca-certificates/ && sudo update-ca-certificates # curl, git, wget, apt…
64
+ sudo systemctl edit thub-client@.service # every instance on this host
65
+ # [Service]
66
+ # Environment=NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/lab-ca.crt
67
+ sudo systemctl restart 'thub-client@*'
68
+ p.small.
69
+ #[strong A client certificate (mutual TLS).] #[code --download-file] can't present one. Fetch the file in
70
+ #[code --command] with #[code curl --cert], with the certificate and key passed as #[code --env] and written
71
+ (#[code umask 077]) to the job's directory, which is deleted with the job. For PKCS#12, use
72
+ #[code curl --cert-type P12 --cert file.p12:password]. One certificate for the whole lab can instead live on the host
73
+ (readable by the service user only), named in a drop-in: every job on that host can then use it.
74
+ +code('mTLS download in the command').
75
+ export CLIENT_CERT="$(cat thub-ci.crt)" CLIENT_KEY="$(cat thub-ci.key)" # PEM, from your CI's secret store
76
+ thub run --type hw --env CLIENT_CERT --env CLIENT_KEY \
77
+ --command 'umask 077 && printf "%s\n" "$CLIENT_CERT" > "$THUB_WORK_DIR/c.pem" && printf "%s\n" "$CLIENT_KEY" > "$THUB_WORK_DIR/k.pem" &&
78
+ curl -fsSL --cert "$THUB_WORK_DIR/c.pem" --key "$THUB_WORK_DIR/k.pem" -o app.bin https://artifactory.lab/fw-local/app/1.4.0/app.bin &&
79
+ st-flash --reset write app.bin 0x08000000 && ./ci/test.sh' --wait
80
+
81
+ h3.h6 JFrog Artifactory: service authentication
82
+ .table-responsive
83
+ table.table.table-sm.small.align-middle
84
+ thead
85
+ tr
86
+ th Credential
87
+ th Sent as
88
+ th Notes
89
+ tbody
90
+ tr
91
+ td #[strong Access token] (scoped, expiring)
92
+ td: code Authorization: Bearer <token>
93
+ td Recommended. A service user with read access to inputs and deploy access to outputs.
94
+ tr
95
+ td Reference token
96
+ td: code Authorization: Bearer <token>
97
+ td Short form of an access token, for CI variables with a length limit.
98
+ tr
99
+ td Identity token / password
100
+ td: code -u svc-thub:<token>
101
+ td For tools that only speak basic auth: #[code docker login], #[code pip].
102
+ tr
103
+ td API key
104
+ td: code X-JFrog-Art-Api
105
+ td Deprecated and turned off in current versions. Migrate.
106
+ tr
107
+ td Anonymous read
108
+ td —
109
+ td Public repositories only. Then #[code --download-file] is enough.
110
+ p.small.
111
+ Best: a #[strong short-lived token per run], minted in the CI step from a service token, so a leaked token expires on its own.
112
+ On GitHub Actions, JFrog's OIDC integration (#[code jfrog/setup-jfrog-cli] with an #[code oidc-provider-name]) needs no stored secret at all.
113
+ +code('CI step: a one-hour token for the job').
114
+ # CI step: ART_SERVICE_TOKEN is a CI secret for the service user svc-thub
115
+ export ART_TOKEN=$(curl -fsS -H "Authorization: Bearer $ART_SERVICE_TOKEN" -X POST \
116
+ -d scope=applied-permissions/user -d expires_in=3600 \
117
+ https://artifactory.example.com/access/api/v1/tokens | jq -r .access_token)
118
+ thub run --type hw --env ART_TOKEN --command '…' --wait
119
+ +code('curl: download, test, upload with checksum, list').
120
+ thub run --type hw --env ART_TOKEN \
121
+ --command 'ART=https://artifactory.example.com/artifactory
122
+ H() { printf "header = \"Authorization: Bearer %s\"\n" "$ART_TOKEN"; } # via curl -K -, so not visible in ps
123
+ H | curl -fsSL -K - -o app.bin "$ART/fw-local/app/1.4.0-42/app.bin" &&
124
+ st-flash --reset write app.bin 0x08000000 && ./ci/test.sh; rc=$?
125
+ url="$ART/qa-local/$THUB_JOB_ID/junit.xml"
126
+ H | curl -fsS -K - -H "X-Checksum-Sha256: $(sha256sum results/junit.xml | cut -d" " -f1)" -T results/junit.xml "$url" &&
127
+ sh ci/artifacts.sh junit.xml "$url" results/junit.xml
128
+ exit $rc' --wait
129
+ +code('JFrog CLI, configured from --env, nothing left in the home directory').
130
+ export JF_ACCESS_TOKEN="$ART_TOKEN"
131
+ thub run --type sw --env JF_URL=https://artifactory.example.com --env JF_ACCESS_TOKEN \
132
+ --command 'export JFROG_CLI_HOME_DIR="$THUB_WORK_DIR/.jfrog" CI=true
133
+ jf rt dl "fw-local/app/1.4.0-42/*.bin" downloads/ --flat && ./ci/test.sh; rc=$?
134
+ jf rt u "results/*.xml" "qa-local/$THUB_JOB_ID/" --flat
135
+ exit $rc' --wait
136
+ +code('Signed URL (Enterprise+): --download-file with nothing else').
137
+ IMAGE_URL=$(curl -fsS -H "Authorization: Bearer $ART_SERVICE_TOKEN" -H "Content-Type: application/json" -X POST \
138
+ -d '{"repo_path":"/fw-local/app/1.4.0-42/app.bin","valid_for_secs":3600}' \
139
+ https://artifactory.example.com/artifactory/api/signed/url)
140
+ thub run --type hw --download-file "$IMAGE_URL" --command 'st-flash --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh' --wait
141
+ +code('The same token for Docker, PyPI and npm repositories').
142
+ # Docker registry (see §7.2): the token as the password
143
+ echo "$ART_TOKEN" | docker login artifactory.example.com -u svc-thub --password-stdin
144
+ docker pull artifactory.example.com/docker-local/team/test-runner:1.4
145
+ # PyPI remote/virtual repository
146
+ pip install -r requirements.txt --index-url "https://svc-thub:$ART_TOKEN@artifactory.example.com/artifactory/api/pypi/pypi/simple"
147
+ # npm: a project-local .npmrc in the work directory, not ~/.npmrc
148
+ printf '//artifactory.example.com/artifactory/api/npm/npm/:_authToken=%s\n' "$ART_TOKEN" > .npmrc
149
+ npm ci --registry https://artifactory.example.com/artifactory/api/npm/npm/
150
+ p.small.
151
+ Nexus raw repositories take #[code curl -u user:password --upload-file]. GitLab's generic packages take #[code curl -H "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file].
152
+
153
+ h3.h6 AWS S3 (and MinIO, Ceph, Cloudflare R2, Wasabi)
154
+ p.small.
155
+ Pre-sign inputs in CI, so the job needs no AWS credentials. Pass short-lived session credentials from the CI step's role
156
+ (OIDC) for outputs. A pre-signed link lasts at most 7 days. For S3-compatible storage, add #[code --endpoint-url] to #[code aws].
157
+ +code('Inputs: pre-signed in the CI step').
158
+ # in the CI step, which has AWS access (e.g. GitHub/GitLab OIDC → an IAM role)
159
+ IMAGE_URL=$(aws s3 presign "s3://fw-builds/app/$SHA/app.bin" --expires-in 3600)
160
+ thub run --type hw --download-file "$IMAGE_URL" --command 'st-flash --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh' --wait
161
+ +code('Outputs: aws s3 cp with session credentials').
162
+ thub run --type sw --env AWS_ACCESS_KEY_ID --env AWS_SECRET_ACCESS_KEY --env AWS_SESSION_TOKEN --env AWS_DEFAULT_REGION=eu-central-1 \
163
+ --command 'aws s3 cp "s3://fw-builds/app/1.4.0/app.elf" . && ./ci/test.sh; rc=$?
164
+ aws s3 cp --recursive results/ "s3://qa-results/$THUB_JOB_ID/" &&
165
+ sh ci/artifacts.sh report.html "$(aws s3 presign "s3://qa-results/$THUB_JOB_ID/report.html" --expires-in 604800)"
166
+ exit $rc' --wait
167
+
168
+ h3.h6 Google Drive
169
+ p.small.
170
+ Use #[a(href="https://rclone.org/drive/" target="_blank" rel="noopener") rclone] with a #[strong service account] that's a
171
+ member of a #[strong shared drive] (service accounts have no storage of their own). rclone takes its configuration from
172
+ environment variables. #[code rclone link] makes a file readable by anyone with the link; leave it out for private reports.
173
+ Public #[code uc?export=download] links work with #[code --download-file] only for small files.
174
+ +code('rclone with a service account').
175
+ # GDRIVE_SA: the service account's JSON key, as one line (a CI secret)
176
+ thub run --type sw --env GDRIVE_SA --env RCLONE_CONFIG_GD_TYPE=drive --env RCLONE_CONFIG_GD_SCOPE=drive \
177
+ --env RCLONE_CONFIG_GD_TEAM_DRIVE=0ABCdEfGhIjKlUk9PVA \
178
+ --command 'export RCLONE_CONFIG_GD_SERVICE_ACCOUNT_CREDENTIALS="$GDRIVE_SA"
179
+ rclone copyto "gd:firmware/1.4.0/app.bin" app.bin && ./ci/test.sh app.bin; rc=$?
180
+ rclone copy results/ "gd:qa/$THUB_JOB_ID/" &&
181
+ sh ci/artifacts.sh report.html "$(rclone link "gd:qa/$THUB_JOB_ID/report.html")"
182
+ exit $rc' --wait
183
+ +code('Or the Drive API with curl and an OAuth token from the CI step').
184
+ curl -fsSL -H "Authorization: Bearer $GDRIVE_TOKEN" -o app.bin "https://www.googleapis.com/drive/v3/files/$FILE_ID?alt=media&supportsAllDrives=true"
185
+ curl -fsS -H "Authorization: Bearer $GDRIVE_TOKEN" \
186
+ -F "metadata={\"name\":\"report.html\",\"parents\":[\"$FOLDER_ID\"]};type=application/json" -F "file=@results/report.html" \
187
+ "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&supportsAllDrives=true"
188
+
189
+ h3.h6 FTP, FTPS and SFTP
190
+ p.small.
191
+ #[code curl] speaks all three. Prefer SFTP or FTPS (#[code --ssl-reqd]); plain FTP sends the password in clear text. Pin the
192
+ SFTP host key with #[code --hostpubsha256] (from #[code ssh-keyscan host | ssh-keygen -lf -], without #[code SHA256:]).
193
+ FTP and SFTP links aren't #[code http(s)], so list the server's HTTPS view, if it has one.
194
+ +code('SFTP download and upload (FTPS and key auth in the comments)').
195
+ thub run --type hw --env SFTP_USER --env SFTP_PASSWORD --env SFTP_HOSTKEY \
196
+ --command 'auth() { printf "user = \"%s:%s\"\n" "$SFTP_USER" "$SFTP_PASSWORD"; } # read by curl -K -, so not visible in ps
197
+ auth | curl -fsS -K - --hostpubsha256 "$SFTP_HOSTKEY" -o app.bin "sftp://files.lab/fw/1.4.0/app.bin" &&
198
+ st-flash --reset write app.bin 0x08000000 && ./ci/test.sh; rc=$?
199
+ auth | curl -fsS -K - --hostpubsha256 "$SFTP_HOSTKEY" --ftp-create-dirs -T results/junit.xml "sftp://files.lab/qa/$THUB_JOB_ID/"
200
+ exit $rc' --wait
201
+ # FTPS: auth | curl -fsS -K - --ssl-reqd --ftp-create-dirs -T results/junit.xml "ftp://files.lab/qa/$THUB_JOB_ID/"
202
+ # SSH key auth: printf '%s\n' "$SFTP_KEY" > "$THUB_WORK_DIR/.key" && chmod 600 "$THUB_WORK_DIR/.key" &&
203
+ # curl --key "$THUB_WORK_DIR/.key" -u "$SFTP_USER:" --hostpubsha256 "$SFTP_HOSTKEY" …
204
+
205
+ h3.h6 Any other HTTP storage: curl and your own authentication
206
+ +code('curl authentication patterns').
207
+ H() { printf 'header = "%s"\n' "$1"; } # headers via curl -K -, kept out of ps
208
+ H "Authorization: Bearer $API_TOKEN" | curl -fsSL -K - -o app.bin "$URL" # bearer token
209
+ H "X-API-Key: $API_KEY" | curl -fsSL -K - -o app.bin "$URL" # API-key header
210
+ printf 'user = "%s:%s"\n' "$USER_NAME" "$USER_PASS" | curl -fsSL -K - -o app.bin "$URL" # basic auth
211
+ curl -fsSL --netrc-file "$THUB_WORK_DIR/.netrc" -o app.bin "$URL" # a .netrc the command wrote from --env values
212
+ curl -fsSL --cert "$THUB_WORK_DIR/c.pem" --key "$THUB_WORK_DIR/k.pem" -o app.bin "$URL" # mutual TLS
213
+ # OAuth2 client credentials: get a token, then use it as a bearer token
214
+ TOKEN=$(curl -fsS -d grant_type=client_credentials -u "$CLIENT_ID:$CLIENT_SECRET" https://auth.example.com/oauth/token | jq -r .access_token)
215
+ # uploads: PUT (-T file), or a form POST (-F "file=@results/report.html")
216
+ p.small.mb-0.
217
+ Once the patterns settle, move them into #[code ci/fetch.sh] / #[code ci/publish.sh] in the tests repository, so every job's
218
+ #[code --command] stays one line. The CI examples in #[a(href="#ci") CI/CD integration] use this storage to bring JUnit reports back.
@@ -9,10 +9,18 @@
9
9
  ['The job is rejected with `422`', 'No registered Client can ever match its type, labels, group or client. Compare `thub resources` with your `--type` and `--label`s; your group (set on the dashboard, see `thub whoami`) must have a Client of that kind.'],
10
10
  ['The job stays `QUEUED`', 'Matching Clients are all `BUSY`, locked (`local`), in `MAINTENANCE` or offline, or the job is pinned with `--client`. It times out after `--timeout`.'],
11
11
  ['`ERROR` during prepare', 'A download failed; the job log names the URL and the reason.'],
12
+ ['`Download failed … (SELF_SIGNED_CERT_IN_CHAIN)` or `UNABLE_TO_GET_ISSUER_CERT_LOCALLY`', 'The server\'s certificate is from a CA the Client doesn\'t trust. Node.js ignores the system CA store: add `Environment=NODE_EXTRA_CA_CERTS=/path/ca.crt` with `sudo systemctl edit thub-client@.service` and restart the Clients. A download that needs a client certificate can\'t use `--download-file` at all: fetch it with `curl --cert` in `--command` (Artifact storage and certificates).'],
13
+ ['`Host key verification failed` on a git clone', 'The job\'s `known_hosts` has no key for that server (or a different one). Pass the output of `ssh-keyscan <host>` as `--env GIT_KNOWN_HOSTS` and use it with `UserKnownHostsFile` (Using git). `Permission denied (publickey)` means the host is fine, but the deploy key isn\'t on that repository.'],
14
+ ['`docker login`: unauthorized', 'Check the user the registry expects: `AWS` for ECR, `oauth2accesstoken` for Google, `gitlab-ci-token` with `CI_JOB_TOKEN` for GitLab (Using Docker). ECR and Google tokens expire after 12 h and 1 h, so mint them in the CI step that submits the job.'],
12
15
  ['A git clone or docker pull in the command fails', 'They run in your `--command`, with the credentials you pass as `--env` (see Using git / Using Docker). `git` or `docker` must be installed on that Client host; the Client itself needs neither. Try the command with `--dry-run` to see its environment.'],
13
16
  ['`--git-repo` / `--docker-image`: unknown option, or the job is refused', 'They were removed: clone the repository and run containers in `--command`, passing tokens with `--env`. An older Agent that still sends them is refused with that message; update it (`thub self-update`).'],
14
17
  ['Permission denied on `/dev/ttyUSB*` or `docker.sock`', 'Docker or the device groups were added after the Client was installed. Re-run `sudo npm i -g @andrian.yablonskyy/thub-client` so the unit gets the `dialout`/`plugdev`/`docker` groups, then restart.'],
15
18
  ['A configured device is shown as missing', 'Check the `devpath` with `udevadm info -a -n <dev>`, then `thub-client udev --print`; restart the Client to regenerate the udev rules.'],
19
+ ['`thub power` is refused', 'It says why: only the job\'s owner may switch power (`403`, even for an admin; `thub whoami` shows whose key you use); the job must be `PREPARING` or `RUNNING`; the Client must list ports in `hw-devices.usbPower.ports` and have `uhubctl` installed (restart it after installing, so it reports it). `--port` is a position in that list, not the hub\'s port number.'],
20
+ ['USB power: `No compatible devices detected!`', 'Either the hub can\'t switch per-port power (`sudo uhubctl` doesn\'t list it), or the Client\'s user may not switch it: install its udev rules with `sudo thub-client --config <path> udev` and check its service has the `plugdev` group (reinstall the Client if `plugdev` was created after it).'],
21
+ ['USB power is off, but the board stays on (or half-boots after a reset)', 'Something else powers it: a second USB cable (ST-Link and the board\'s own USB), a debugger or an external supply. Every cable that powers it needs a switched port in `usbPower.ports`. Some hubs also feed power back from upstream. A board that half-boots needs a longer delay: `--delay 3`.'],
22
+ ['A job can\'t reach its smart socket or PDU', 'The job\'s command runs on the Client host, so the device must be reachable from there: `curl -sI http://<device>/` as the Client\'s user. Tools such as `curl` or `snmpset` must be installed on that host. `BENCH_POWER is not set`: add the instance\'s systemd drop-in and restart it (`systemctl show thub-client@<instance> -p Environment`).'],
23
+ ['A board on a socket or PDU stays on after a canceled job', 'On cancel the Client sends `SIGTERM` to the job\'s shell only, then `SIGKILL` after 10 s. Start the wrapper with `exec`, run the tests in the background with `wait`, trap `TERM`, and finish within 10 s. Add a timer on the device (Shelly `toggle_after`, Tasmota `PulseTime`) as a backstop.'],
16
24
  ['Job `LOST`', 'The Client missed 3 heartbeats (network, reboot, crash). With `requeueOnLost` the job is retried once on another Client.'],
17
25
  ['A job can\'t write to `~` (read-only file system)', 'Under systemd the Client can only write to its state directory. Write to `$THUB_WORK_DIR`, and set `DOCKER_CONFIG` there for `docker login`.'],
18
26
  ['Live logs don\'t stream through the proxy', 'Turn off response buffering for the Coordinator (nginx: `proxy_buffering off;`, Caddy: `flush_interval -1`).'],
@@ -0,0 +1,147 @@
1
+ +section('usb-power', 'USB port power (uhubctl)', 'power')
2
+ p.
3
+ An HW Client can switch the power of the USB ports its boards hang off, with
4
+ #[a(href="https://github.com/mvp/uhubctl" target="_blank" rel="noopener") uhubctl]. Use it to power-cycle a
5
+ hung board, start every job from a cold boot, or keep boards off between jobs. It needs a USB hub with
6
+ #[strong per-port power switching] (uhubctl's README lists hubs known to work) and #[code sudo apt install uhubctl]
7
+ on the Client host.
8
+
9
+ h3.h6 Setting it up
10
+ ol.small
11
+ li
12
+ | Find each board's hub and port. Run #[code uhubctl] with no arguments. The hub's #[strong location] is the
13
+ | word after #[code hub], and the #[strong port] is the number after #[code Port]. #[code ppps] means the hub
14
+ | switches each port on its own, and #[code ganged] means it switches all its ports together. A USB3 hub shows up twice
15
+ | (#[code 1-1.4] and #[code 2-1.4] below): use the half the board is on, and uhubctl switches the other with it.
16
+ +code('Client host').
17
+ $ sudo uhubctl
18
+ Current status for hub 1-1.4 [2109:2817 VIA Labs, Inc. USB2.0 Hub, USB 2.10, 4 ports, ppps]
19
+ Port 1: 0100 power
20
+ Port 2: 0103 power enable connect [0483:3748 STMicroelectronics STM32 STLink 066DFF485457725187092834]
21
+ Port 3: 0103 power enable connect [0483:3748 STMicroelectronics STM32 STLink 0670FF505055877267183229]
22
+ Port 4: 0000 off
23
+ Current status for hub 2-1.4 [2109:0817 VIA Labs, Inc. USB3.0 Hub, USB 3.00, 4 ports, ppps]
24
+ Port 1: 02a0 power 5gbps Rx.Detect
25
+ li
26
+ | Try it by hand:
27
+ +code('Client host').
28
+ sudo uhubctl -l 1-1.4 -p 2 -a off # the board on port 2 goes dark
29
+ sudo uhubctl -l 1-1.4 -p 2 -a on
30
+ li
31
+ | List the ports under #[code hw-devices.usbPower.ports] in the Client's config, up to 8. Each port's number is
32
+ | its #[strong position in this list] (from 1), and that's what #[code --port] takes. It isn't the hub's own
33
+ | port number: below, #[code --port 2] is hub #[code 1-1.4] port 3.
34
+ +code('~/.config/thub/dut1.json').
35
+ "hw-devices": {
36
+ "stlinks": [{ "index": 1, "devpath": "1.4.2" }, { "index": 2, "devpath": "1.4.3" }],
37
+ "usbPower": {
38
+ "ports": [
39
+ { "hub": "1-1.4", "port": 2 },
40
+ { "hub": "1-1.4", "port": 3 }
41
+ ]
42
+ }
43
+ }
44
+ li
45
+ | Restart the Client. It installs a udev rule that lets the #[code plugdev] group (which its service has)
46
+ | switch hub ports without root, and reports the ports to the Coordinator, along with whether uhubctl is
47
+ | installed. Then check:
48
+ +code('Client host').
49
+ sudo systemctl restart thub-client@dut1
50
+ thub-client --config ~/.config/thub/dut1.json power status
51
+ # 1. hub 1-1.4 port 2: on (Port 2: 0103 power enable connect [...])
52
+ # 2. hub 1-1.4 port 3: on (Port 3: 0103 power enable connect [...])
53
+ +note.
54
+ Saving a runner's devices on its #[strong Config] tab keeps #[code usbPower] as it is. The ports are only edited in the
55
+ config file.
56
+
57
+ h3.h6 What the actions do
58
+ p.small.
59
+ Every action switches all the listed ports, unless #[code --port] picks one. The Client runs one power request at a
60
+ time, so a job's own reset, its owner's and one from the bench never overlap.
61
+ .table-responsive
62
+ table.table.table-sm.small.align-middle
63
+ thead
64
+ tr
65
+ th(style="width: 12%") Action
66
+ th What the Client runs
67
+ tbody
68
+ tr
69
+ td: code on
70
+ td: code uhubctl -l 1-1.4 -p 2,3 -a on
71
+ tr
72
+ td: code off
73
+ td: code uhubctl -l 1-1.4 -p 2,3 -a off
74
+ tr
75
+ td: code reset
76
+ td
77
+ | #[code uhubctl … -a off], then a wait of the #[strong reset delay] (1 s unless given, 0–60 s, fractions
78
+ | allowed), then #[code uhubctl … -a on]. Give boards that are slow to discharge a longer delay.
79
+ tr
80
+ td: code status
81
+ td #[code uhubctl -l 1-1.4 -p 2,3], each port's state from it (Client host only)
82
+
83
+ h3.h6 Three ways to switch the power
84
+ .row.g-3.mb-3
85
+ .col-md-4
86
+ .card.h-100
87
+ .card-header.small
88
+ i.bi.bi-pc.me-1
89
+ strong On the Client host
90
+ .card-body.small
91
+ p.
92
+ #[code thub-client power on|off|reset|status [--port N] [--delay SEC]]. It goes through the running Client.
93
+ If a job is running, its log records the action.
94
+ .col-md-4
95
+ .card.h-100
96
+ .card-header.small
97
+ i.bi.bi-play-circle.me-1
98
+ strong At a job's start and end
99
+ .card-body.small
100
+ p.mb-2.
101
+ #[code thub run --power-on-start … --power-on-end … [--power-reset-delay SEC]] (HW jobs). The start action runs
102
+ after the downloads, before the command; if it fails, the job ends #[code ERROR].
103
+ p.mb-0.
104
+ The end action runs however the job ends (failed and canceled jobs too), before the result is reported.
105
+ If it fails, the failure is logged and the verdict stays as it was.
106
+ .col-md-4
107
+ .card.h-100
108
+ .card-header.small
109
+ i.bi.bi-lightning.me-1
110
+ strong While your job runs
111
+ .card-body.small
112
+ p.mb-2.
113
+ #[code thub power on|off|reset &lt;jobId> [--delay SEC] [--port N]]. Power-cycle a hung board straight away,
114
+ without canceling the job.
115
+ p.mb-0.
116
+ Only the job's owner can do this, not even an admin, and only while the job is #[code PREPARING] or
117
+ #[code RUNNING]. It reaches the Client within about a second.
118
+
119
+ +code('Cold-boot the board before the job, leave it off afterwards').
120
+ thub run --type hw --label board:nucleo-f401re \
121
+ --download-file "$IMAGE_URL" \
122
+ --command 'st-flash --reset write "$THUB_DOWNLOAD_1" 0x08000000 && ./ci/test.sh' \
123
+ --power-on-start reset --power-reset-delay 2 \
124
+ --power-on-end off \
125
+ --wait
126
+ +code('The board hung: power-cycle it from another terminal').
127
+ $ thub power reset M-00131
128
+ USB power reset, 1 s off sent to lab-hw-01 for job M-00131 — the job's log shows when it's done.
129
+ $ thub power reset M-00131 --port 2 --delay 3 # just the Client's second port, 3 s off
130
+ $ thub power off M-00131 && thub power on M-00131
131
+ +code('What the job log shows').
132
+ [runner] job start: USB power reset
133
+ [runner] USB power reset: 1-1.4:2, 1-1.4:3 (off 2 s)
134
+ [runner] preparing DUT
135
+ [runner] USB power reset (port 2) requested by alice (thub power)
136
+ [runner] USB power reset: 1-1.4:3 (off 3 s)
137
+ [runner] job end: USB power off
138
+ [runner] USB power off: 1-1.4:2, 1-1.4:3
139
+ +code('From the test script itself, between test cases (the Agent must be installed on the Client host)').
140
+ thub run --type hw --env AGENT_URL --env AGENT_KEY --command './ci/test.sh' --wait
141
+ # in ci/test.sh — the key must be the one that submitted the job:
142
+ thub --url "$AGENT_URL" --key "$AGENT_KEY" power reset "$THUB_JOB_ID" --delay 2
143
+ p.small.mb-0.
144
+ A dry run (#[code --dry-run]) lists the uhubctl calls instead of making them. The command sees what was asked for
145
+ as #[code JOB_POWER_ON_START], #[code JOB_POWER_ON_END] and #[code JOB_POWER_RESET_DELAY]. Problems are covered
146
+ under #[a(href="#troubleshooting") Troubleshooting]. Boards on a smart socket or a PDU outlet are switched by the job
147
+ itself: see #[a(href="#lab-devices") Smart sockets, PDUs and other lab devices].
@@ -12,11 +12,14 @@ block content
12
12
  { id: 'agent-setup', label: 'Agent setup' },
13
13
  { id: 'client-setup', label: 'Client setup' },
14
14
  { id: 'client-machines', label: 'Client machines (HW / SW)' },
15
+ { id: 'usb-power', label: 'USB port power (uhubctl)' },
16
+ { id: 'lab-devices', label: 'Smart sockets, PDUs, devices' },
15
17
  { id: 'docker', label: 'Using Docker' },
16
18
  { id: 'git', label: 'Using git' },
17
19
  { id: 'agent-cli', label: 'Agent CLI reference' },
18
20
  { id: 'env', label: 'Environment variables' },
19
- { id: 'ci', label: 'CI/CD (GitHub Actions)' },
21
+ { id: 'storage', label: 'Artifact storage & certificates' },
22
+ { id: 'ci', label: 'CI/CD (GitHub, GitLab, Bitbucket, Jenkins)' },
20
23
  { id: 'troubleshooting', label: 'Troubleshooting' }
21
24
  ]
22
25
 
@@ -58,10 +61,13 @@ block content
58
61
  include _agent-setup
59
62
  include _client-setup
60
63
  include _client-machines
64
+ include _usb-power
65
+ include _lab-devices
61
66
  include _docker
62
67
  include _git
63
68
  include _agent-cli
64
69
  include _env
70
+ include _storage
65
71
  include _ci
66
72
  include _troubleshooting
67
73
 
package/views/layout.pug CHANGED
@@ -29,12 +29,13 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
29
29
  data-bs-title=`Coordinator v${coordinatorVersion} · thub-common v${commonVersion} (validates job specs)`
30
30
  )
31
31
  | ver. #{coordinatorVersion}
32
- //- Without --self-update (or for non-admins): just the notice.
33
- if user && updates.coordinatorUpdate && !(can.admin && updates.selfUpdate)
32
+ //- With --self-update, for those who can't update it themselves.
33
+ //- Without it, nothing about Coordinator updates is shown.
34
+ if user && updates.selfUpdate && updates.coordinatorUpdate && !can.admin
34
35
  span.badge.text-bg-warning.ms-1(
35
36
  data-bs-toggle="tooltip"
36
37
  data-bs-placement="bottom"
37
- data-bs-title=updates.selfUpdate ? 'An admin can update the Coordinator from the navbar' : 'Update the Coordinator by hand (thub-admin self-update, npm i -g, or a new container image); self-update from the dashboard is off'
38
+ data-bs-title="An admin can update the Coordinator from the navbar"
38
39
  )= `v${updates.coordinatorUpdate} available`
39
40
  button.navbar-toggler(type="button" data-bs-toggle="collapse" data-bs-target="#nav")
40
41
  span.navbar-toggler-icon
@@ -80,7 +81,9 @@ html(lang="en" data-bs-theme=(user && user.theme) || "auto")
80
81
  )
81
82
  i.bi.bi-question-circle.me-1
82
83
  | Help
83
- if user && can.operate
84
+ //- The Coordinator's own update check: only with --self-update.
85
+ //- (Agents and Clients are checked from their own pages, always.)
86
+ if user && can.operate && updates.selfUpdate
84
87
  li.nav-item.d-flex.align-items-center.me-2
85
88
  form(method="post" action="/updates/check" data-submit-busy)
86
89
  input(type="hidden" name="returnTo" value=currentPath)
@@ -234,10 +234,13 @@ mixin configPanes(r, returnTo, groupsById)
234
234
  +configPane(p, t[0])
235
235
  p.small.text-body-secondary.mb-0 Devices can't be edited here: #{noConfigReport}
236
236
  else
237
+ //- What the tabs don't edit (usbPower, README §8.7) goes back as it was.
238
+ - const keep = Object.fromEntries(Object.entries(current).filter(([k]) => !['stlinks', 'uarts', 'usbs'].includes(k)))
237
239
  form(
238
240
  method="post"
239
241
  action=`/runners/${r.id}/config`
240
242
  data-client-config="hw"
243
+ data-keep=JSON.stringify(keep)
241
244
  data-confirm=`Apply these devices to ${r.name}? The Client writes them to its config file on its next heartbeat and restarts once it has no job running.`
242
245
  data-confirm-title="Save Client devices?"
243
246
  data-confirm-ok="Save"