@aion0/forge 0.10.18 → 0.10.22
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/RELEASE_NOTES.md +4 -4
- package/app/api/agents/route.ts +4 -2
- package/app/api/connectors/route.ts +1 -1
- package/components/HelpTerminal.tsx +23 -6
- package/components/SettingsModal.tsx +3 -11
- package/docs/forge-long-task-watch-design.md +210 -0
- package/docs/tp-automation-api.md +617 -0
- package/lib/agents/index.ts +5 -4
- package/lib/agents/migrate.ts +8 -0
- package/lib/browser-bridge-standalone.ts +13 -4
- package/lib/chat/agent-loop.ts +2 -2
- package/lib/chat/bridge-client.ts +2 -2
- package/lib/chat/protocols/ssh.ts +206 -0
- package/lib/chat/tool-dispatcher.ts +59 -1
- package/lib/connectors/types.ts +62 -2
- package/lib/help-content.ts +51 -0
- package/lib/help-docs/01-settings.md +0 -1
- package/lib/settings.ts +1 -1
- package/lib/telegram-bot.ts +5 -5
- package/package.json +1 -1
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
# TP `/automation` Page — API Reference
|
|
2
|
+
|
|
3
|
+
Reference for the HTTP endpoints used by TP's Automation page
|
|
4
|
+
(`/automation`, source: `frontend/src/pages/Automation/Automation.jsx`)
|
|
5
|
+
and the related upgrade / testbed workflows the page invokes through
|
|
6
|
+
shared infrastructure.
|
|
7
|
+
|
|
8
|
+
All endpoints are mounted under the `adc` Django app:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
<TP-base-url>/adc/<endpoint>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`<TP-base-url>` examples:
|
|
15
|
+
- Production: `https://nac-tp.fortinet-us.com`
|
|
16
|
+
- Test: `http://10.15.33.25:8000`
|
|
17
|
+
- Dev (.11): `http://10.15.33.11:8000`
|
|
18
|
+
|
|
19
|
+
## Authentication
|
|
20
|
+
|
|
21
|
+
Every endpoint requires a JWT in the `Authorization` header:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
Authorization: JWT <token>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Mint a token:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
T=$(curl -s -X POST <TP-base-url>/token-auth/ \
|
|
31
|
+
-H 'Content-Type: application/json' \
|
|
32
|
+
-d '{"username":"<user>","password":"<pw>"}' | jq -r .token)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
In the examples below, `$T` stands for the JWT. Tokens are
|
|
36
|
+
short-lived; on 401 the frontend redirects to SSO, and scripts should
|
|
37
|
+
re-mint.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Endpoints called by the `/automation` page
|
|
42
|
+
|
|
43
|
+
### `GET /adc/automation-verion/`
|
|
44
|
+
|
|
45
|
+
Returns the FortiNAC versions the automation pipeline tracks.
|
|
46
|
+
Populates the version dropdown.
|
|
47
|
+
|
|
48
|
+
Response:
|
|
49
|
+
```json
|
|
50
|
+
{"versions": ["7.4.6", "7.4.7", "7.6.5", "7.6.6"]}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Backed by: `adc.views.dashboard.dashboardviews.get_automation_version`
|
|
54
|
+
|
|
55
|
+
### `GET /adc/get_testcases`
|
|
56
|
+
|
|
57
|
+
Walks the cloned test-framework repo on TP, parses every Python test
|
|
58
|
+
file, and returns a hierarchical tree of modules → test cases.
|
|
59
|
+
|
|
60
|
+
**Side-effect:** pulls latest commits on the `main` branch of the local
|
|
61
|
+
clone before parsing. Calling from a script will rebase TP's local
|
|
62
|
+
repo on `main` — fine in normal use, but worth knowing if a developer
|
|
63
|
+
is hand-testing branches on the TP host.
|
|
64
|
+
|
|
65
|
+
Response shape (truncated):
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"tests": {
|
|
69
|
+
"L2": {
|
|
70
|
+
"test_l2_radius.py": [
|
|
71
|
+
"test_basic_auth",
|
|
72
|
+
"test_radius_attributes"
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
"L3": {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Backed by: `automationview.get_testcases`
|
|
81
|
+
|
|
82
|
+
### `POST /adc/pytest_run`
|
|
83
|
+
|
|
84
|
+
Kicks off a pytest execution on the chosen automation testbed. Creates
|
|
85
|
+
a `PytestExecution` row and returns its id; the actual run is
|
|
86
|
+
asynchronous.
|
|
87
|
+
|
|
88
|
+
Body:
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"user": "alice",
|
|
92
|
+
"lab": "L2Mode_7",
|
|
93
|
+
"testcase": [
|
|
94
|
+
"tests/L2/test_l2_radius.py::test_basic_auth",
|
|
95
|
+
"tests/L2/test_l2_radius.py::test_radius_attributes"
|
|
96
|
+
],
|
|
97
|
+
"argument": "-k 'radius and not flaky' --tb=short -vv"
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Field | Type | Notes |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `user` | string | TP username of the caller. |
|
|
104
|
+
| `lab` | string | AT lab name from `AutomationTBUser` (the same names returned by `get_automation_lab`). Comma-separate multiple labs (`"L2Mode_7,L2Mode_9"`). The caller must already own or be a member of `usedby` on the lab, and no other execution can be `Running`/`Initiating` against it. |
|
|
105
|
+
| `testcase` | **list of strings** | Each entry is a pytest test-id path. The handler iterates the list, prefixes each with the test-framework repo path on TP, and joins them with spaces before invoking pytest. |
|
|
106
|
+
| `argument` | string | **Raw pytest CLI arguments**, injected verbatim between the testcase paths and the framework's `--html=...`/`--rack-file ...` flags. Use this for filters, verbosity, fail-fast, collect-only, marker expressions, etc. |
|
|
107
|
+
|
|
108
|
+
The handler also accepts any **extra** fields in the body — they're
|
|
109
|
+
preserved on the execution record. You can attach tracking metadata
|
|
110
|
+
(`mantis_id`, `jenkins_job`, etc.) without backend changes.
|
|
111
|
+
|
|
112
|
+
#### What gets executed on the testbed
|
|
113
|
+
|
|
114
|
+
The worker generates a shell script of the form:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
git pull origin main
|
|
118
|
+
cd <repo>
|
|
119
|
+
source venv/bin/activate
|
|
120
|
+
export PYTHONPATH=<test-framework>:<tests>
|
|
121
|
+
export DISPLAY=:99
|
|
122
|
+
pytest <testcase paths> <argument> --collect-only
|
|
123
|
+
pytest <testcase paths> <argument> --html=<report> --rack-file <rack> <pytest_options>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
So `argument` is literally the slot for any pytest flag. The worker
|
|
127
|
+
runs `--collect-only` first as a dry-run check, then the real run.
|
|
128
|
+
|
|
129
|
+
#### Examples
|
|
130
|
+
|
|
131
|
+
**Run one test:**
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"user": "alice",
|
|
135
|
+
"lab": "L2Mode_7",
|
|
136
|
+
"testcase": ["tests/L2/test_l2_radius.py::test_basic_auth"],
|
|
137
|
+
"argument": ""
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Run several specific tests, with verbose output and fail-fast:**
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"user": "alice",
|
|
145
|
+
"lab": "L2Mode_7",
|
|
146
|
+
"testcase": [
|
|
147
|
+
"tests/L2/test_l2_radius.py::test_basic_auth",
|
|
148
|
+
"tests/L3/test_l3_vpn.py::test_vpn_login"
|
|
149
|
+
],
|
|
150
|
+
"argument": "-vv -x"
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Run every test in a file, filtered by keyword:**
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"user": "alice",
|
|
158
|
+
"lab": "L2Mode_7",
|
|
159
|
+
"testcase": ["tests/L2/test_l2_radius.py"],
|
|
160
|
+
"argument": "-k 'radius and not flaky'"
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Dry-run / list-only (no test bodies executed by the second pytest invocation):**
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"user": "alice",
|
|
168
|
+
"lab": "L2Mode_7",
|
|
169
|
+
"testcase": ["tests/L2/"],
|
|
170
|
+
"argument": "--collect-only"
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
(Note: `--collect-only` runs twice in this case — once by the worker's
|
|
175
|
+
hardcoded first-line check, once by your explicit flag. Functionally
|
|
176
|
+
identical to a normal collect-only.)
|
|
177
|
+
|
|
178
|
+
#### Response
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{"exec_id": "53"}
|
|
182
|
+
```
|
|
183
|
+
Or on error:
|
|
184
|
+
```json
|
|
185
|
+
{"error": "..."}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
On lab conflict (someone else's run is in progress on that lab), the
|
|
189
|
+
response contains `conflict_labs` and `usable_labs` instead of
|
|
190
|
+
`exec_id`.
|
|
191
|
+
|
|
192
|
+
Backed by: `automationview.pytest_run` → `PytestAction.create_execution_entry`
|
|
193
|
+
|
|
194
|
+
### `POST /adc/get_automation_lab`
|
|
195
|
+
|
|
196
|
+
Lists the automation testbeds the calling user can see (owned or
|
|
197
|
+
shared). Each entry includes a live `status` field computed from
|
|
198
|
+
in-progress executions.
|
|
199
|
+
|
|
200
|
+
Body:
|
|
201
|
+
```json
|
|
202
|
+
{"user": "alice"}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Response (truncated):
|
|
206
|
+
```json
|
|
207
|
+
[
|
|
208
|
+
{
|
|
209
|
+
"name": "L2Mode_7",
|
|
210
|
+
"usedby": "alice,bob",
|
|
211
|
+
"status": "Available"
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`status` is `"Running"` when any `PytestExecution` for this lab has
|
|
217
|
+
status `Running`, else `"Available"`.
|
|
218
|
+
|
|
219
|
+
### `POST /adc/get_test_result`
|
|
220
|
+
|
|
221
|
+
Returns the calling user's recent test executions with computed
|
|
222
|
+
progress percentages.
|
|
223
|
+
|
|
224
|
+
Body:
|
|
225
|
+
```json
|
|
226
|
+
{"user": "alice"}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Response:
|
|
230
|
+
```json
|
|
231
|
+
[
|
|
232
|
+
{
|
|
233
|
+
"id": 53,
|
|
234
|
+
"name": "test001",
|
|
235
|
+
"report_file_path": "media/automation/53/report.html",
|
|
236
|
+
"log_file_path": "media/automation/53/log.txt",
|
|
237
|
+
"status": "Running",
|
|
238
|
+
"progress": 42
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`progress = (pass + fail + skip + error) / total * 100`, rounded.
|
|
244
|
+
|
|
245
|
+
`report_file_path` and `log_file_path` are server-relative; open them
|
|
246
|
+
by prepending `<TP-base-url>` and sending the JWT.
|
|
247
|
+
|
|
248
|
+
### `POST /adc/get_test_execution_by_id`
|
|
249
|
+
|
|
250
|
+
Returns one execution's full record (steps, counts, environment,
|
|
251
|
+
timestamps) for the detail panel.
|
|
252
|
+
|
|
253
|
+
Body:
|
|
254
|
+
```json
|
|
255
|
+
{"id": 53}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Response: serialized `PytestExecution`. Returns 404 if not found.
|
|
259
|
+
|
|
260
|
+
### `POST /adc/test_exec_kill`
|
|
261
|
+
|
|
262
|
+
Aborts a running execution. The row stays in the DB with a terminal
|
|
263
|
+
status; the underlying pytest subprocess is sent SIGTERM.
|
|
264
|
+
|
|
265
|
+
Body:
|
|
266
|
+
```json
|
|
267
|
+
{"id": 53}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### `POST /adc/test_exec_delete`
|
|
271
|
+
|
|
272
|
+
Deletes one or many `PytestExecution` rows and their on-disk
|
|
273
|
+
artifacts. Accepts either a single id or a list.
|
|
274
|
+
|
|
275
|
+
Body (single):
|
|
276
|
+
```json
|
|
277
|
+
{"id": 53}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Body (bulk):
|
|
281
|
+
```json
|
|
282
|
+
{"historyresults": [50, 51, 52]}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### `POST /adc/automation_lock`
|
|
286
|
+
|
|
287
|
+
Marks a testbed as in-use, or releases it. Used to serialize tests
|
|
288
|
+
that need exclusive access to shared hardware.
|
|
289
|
+
|
|
290
|
+
Body:
|
|
291
|
+
```json
|
|
292
|
+
{"action": "lock", "user": "alice", "name": "L2Mode_7"}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`action` is `"lock"` or `"unlock"`.
|
|
296
|
+
|
|
297
|
+
### `POST /adc/automation_tb_set_viewers/`
|
|
298
|
+
|
|
299
|
+
Edits who can *see* a testbed (separate from ownership/usage).
|
|
300
|
+
|
|
301
|
+
Body:
|
|
302
|
+
```json
|
|
303
|
+
{
|
|
304
|
+
"action": "add",
|
|
305
|
+
"tb_name": "L2Mode_7",
|
|
306
|
+
"viewers": ["bob", "carol"]
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`action` is `"add"`, `"remove"`, or `"set"`.
|
|
311
|
+
|
|
312
|
+
### `GET /adc/status-check/`
|
|
313
|
+
|
|
314
|
+
Generic shared-lab status payload used by the status widget rendered
|
|
315
|
+
on the page. Not specific to automation but called from it.
|
|
316
|
+
|
|
317
|
+
Backed by: `adc.views.sharedlab.tbviews.ClassicCombinedStatus`
|
|
318
|
+
|
|
319
|
+
### `GET /user/get_user/<username>`
|
|
320
|
+
|
|
321
|
+
Returns the calling user's profile (role, group, default project) so
|
|
322
|
+
the page can decide what controls to render. Note: outside the `adc`
|
|
323
|
+
namespace.
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## NAC upgrade APIs
|
|
328
|
+
|
|
329
|
+
The `/automation` page does not call these directly, but every
|
|
330
|
+
automation run that targets a specific build relies on the testbed
|
|
331
|
+
having been upgraded first. There are **two modes** of upgrade:
|
|
332
|
+
|
|
333
|
+
| Mode | What it targets | Endpoints |
|
|
334
|
+
|---|---|---|
|
|
335
|
+
| **Device mode** | One specific `ip` | `nac-upgrade/`, `nac-upgrade-version-snapshot/`, `nac-upgrade-snapshot/`, `check-upgrade-status/` |
|
|
336
|
+
| **Testbed mode** | Every NAC/NCM device in a testbed (discovered from `deployinfo`) | `nac-upgrade-testbed/` |
|
|
337
|
+
|
|
338
|
+
Both modes share the same `upgrade_type` axis:
|
|
339
|
+
|
|
340
|
+
| `upgrade_type` | Required extra field | Meaning |
|
|
341
|
+
|---|---|---|
|
|
342
|
+
| `GA` | (none) | Pull the latest GA image from the build server |
|
|
343
|
+
| `build` | `build_number` | Download a specific build (e.g. `7.6.6.0123`) and install it |
|
|
344
|
+
| `file` | `file_uploaded` (multipart) | Install from an uploaded image |
|
|
345
|
+
| `command` | `command` | Run a raw upgrade CLI command (advanced/manual) |
|
|
346
|
+
|
|
347
|
+
Upgrade work is **asynchronous** in both modes — the call returns once
|
|
348
|
+
the job is queued. Use the device-mode status endpoint to poll.
|
|
349
|
+
|
|
350
|
+
### Device mode
|
|
351
|
+
|
|
352
|
+
#### `POST /adc/nac-upgrade/`
|
|
353
|
+
|
|
354
|
+
Upgrade a single FortiNAC device. Accepts `multipart/form-data` so it
|
|
355
|
+
can carry the uploaded image file.
|
|
356
|
+
|
|
357
|
+
Form fields:
|
|
358
|
+
- `ip` — target device IP (required)
|
|
359
|
+
- `upgrade_type` — `GA` / `build` / `file` / `command`
|
|
360
|
+
- `build_number` — when `upgrade_type=build`
|
|
361
|
+
- `file_uploaded` — when `upgrade_type=file` (image file as multipart)
|
|
362
|
+
- `command` — when `upgrade_type=command`
|
|
363
|
+
|
|
364
|
+
Response:
|
|
365
|
+
```json
|
|
366
|
+
{"status": "success", "message": "..."}
|
|
367
|
+
```
|
|
368
|
+
HTTP `202` on success, `400` on failure.
|
|
369
|
+
|
|
370
|
+
Backed by: `nacviews.nac_upgrade`
|
|
371
|
+
|
|
372
|
+
#### `POST /adc/nac-upgrade-version-snapshot/`
|
|
373
|
+
|
|
374
|
+
Take a pre-upgrade snapshot of the device's current image so it can be
|
|
375
|
+
reverted later. Returns once the snapshot job has been queued.
|
|
376
|
+
|
|
377
|
+
Body:
|
|
378
|
+
```json
|
|
379
|
+
{
|
|
380
|
+
"ip": "10.15.40.42",
|
|
381
|
+
"dev_name": "fortinac01",
|
|
382
|
+
"build_number": "7.6.6.0123",
|
|
383
|
+
"upgrade_type": "build",
|
|
384
|
+
"major_version": "7.6",
|
|
385
|
+
"minor_version": "6",
|
|
386
|
+
"rp_prefix": "rp",
|
|
387
|
+
"command": "<optional, when upgrade_type=command>"
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Backed by: `nacupgradeview.nac_upgrade_version_snapshot`
|
|
392
|
+
|
|
393
|
+
#### `POST /adc/nac-upgrade-snapshot/`
|
|
394
|
+
|
|
395
|
+
Upgrade *with* automatic snapshot/revert. Same payload as
|
|
396
|
+
`nac-upgrade-version-snapshot/` plus:
|
|
397
|
+
|
|
398
|
+
- `revert_snapshot` — `true` / `false`; when `true`, the device is
|
|
399
|
+
reverted to the snapshot if the upgrade fails.
|
|
400
|
+
|
|
401
|
+
Backed by: `nacupgradeview.nac_upgrade_snapshot`
|
|
402
|
+
|
|
403
|
+
#### `POST /adc/check-upgrade-status/`
|
|
404
|
+
|
|
405
|
+
Poll the upgrade status of a single device.
|
|
406
|
+
|
|
407
|
+
Body:
|
|
408
|
+
```json
|
|
409
|
+
{"ip": "10.15.40.42"}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Response: state of the most recent upgrade task for that IP. Shape:
|
|
413
|
+
```json
|
|
414
|
+
{
|
|
415
|
+
"is_upgrading": false,
|
|
416
|
+
"last_task_status": "SUCCESS",
|
|
417
|
+
"last_task_type": "build",
|
|
418
|
+
"last_build_number": "7.6.6.0123",
|
|
419
|
+
"last_image_name": "FortiNAC-F-7.6.6.0123.out",
|
|
420
|
+
"last_major_version": "7.6",
|
|
421
|
+
"last_minor_version": "6",
|
|
422
|
+
"updated_at": "2026-05-28T11:42:01.123456+00:00",
|
|
423
|
+
"log": "...full log tail...",
|
|
424
|
+
"ip": "10.15.40.42"
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
`is_upgrading` is `true` when `last_task_status` is `PENDING` or
|
|
429
|
+
`PROGRESS`. The endpoint works for both modes — see the table at the
|
|
430
|
+
top of the section for which testbed-mode `upgrade_type`s actually
|
|
431
|
+
write to this table — but read the polling caveats below carefully.
|
|
432
|
+
|
|
433
|
+
##### Polling caveat — testbed-mode `build`
|
|
434
|
+
|
|
435
|
+
`run_download_async` (the worker that handles `upgrade_type=build` in
|
|
436
|
+
testbed mode) does **not** set `last_task_status` to `PENDING` or
|
|
437
|
+
`PROGRESS` while running. It only writes `SUCCESS` on completion.
|
|
438
|
+
Effect: while a testbed-mode build upgrade is in flight,
|
|
439
|
+
`is_upgrading` returns `false` even though the worker is actively
|
|
440
|
+
downloading + restoring on the device.
|
|
441
|
+
|
|
442
|
+
To get live progress mid-run for testbed-mode build upgrades, read
|
|
443
|
+
the `log` field instead — `run_download_async` appends to it
|
|
444
|
+
continuously (download start, restore start, VM CLI output, success).
|
|
445
|
+
|
|
446
|
+
> **Known bug (worth fixing as a separate task):** `run_download_async`
|
|
447
|
+
> in `backend/adc/views/sharedlab/nacviews.py` should set
|
|
448
|
+
> `last_task_status = "PROGRESS"` immediately after `get_or_create`
|
|
449
|
+
> at the start of the worker, and `"FAILURE"` in its `except` branch
|
|
450
|
+
> (it currently only writes `"SUCCESS"` on the happy path). With that
|
|
451
|
+
> fix, `check-upgrade-status/` would return an accurate `is_upgrading`
|
|
452
|
+
> flag for testbed-mode build upgrades and surface failures without
|
|
453
|
+
> requiring the caller to parse the log field.
|
|
454
|
+
|
|
455
|
+
##### Polling caveat — testbed-mode `GA`
|
|
456
|
+
|
|
457
|
+
`upgrade_type=GA` in `nac_upgrade_testbed` is a stub — it returns
|
|
458
|
+
`{"upgrade_type": "GA"}` immediately and never touches `SingleDevice`.
|
|
459
|
+
This endpoint will report `"Device not found"` (404) for IPs that have
|
|
460
|
+
never been upgraded by another mode. There is no work to poll.
|
|
461
|
+
|
|
462
|
+
##### Polling caveat — testbed-mode `file` and `command`
|
|
463
|
+
|
|
464
|
+
These branches run synchronously inside the HTTP request and only
|
|
465
|
+
write to `SingleDevice` *after* the work finishes. The request itself
|
|
466
|
+
blocks until then (file restore can take minutes), so polling
|
|
467
|
+
`check-upgrade-status/` for in-flight progress isn't useful for these
|
|
468
|
+
types — by the time you can issue a separate poll, the upgrade is
|
|
469
|
+
already done.
|
|
470
|
+
|
|
471
|
+
Backed by: `nacupgradeview.check_upgrade_status`
|
|
472
|
+
|
|
473
|
+
### Testbed mode
|
|
474
|
+
|
|
475
|
+
#### `POST /adc/nac-upgrade-testbed/`
|
|
476
|
+
|
|
477
|
+
Upgrade every NAC/NCM device in a testbed at once. Accepts
|
|
478
|
+
`multipart/form-data` to carry an optional uploaded image.
|
|
479
|
+
|
|
480
|
+
The handler reads `deployinfo` (JSON), finds every key matching
|
|
481
|
+
`dev<N>` whose value contains `nac` or `ncm`, and pairs it with the
|
|
482
|
+
corresponding `ip<N>` to build the upgrade list — then runs the
|
|
483
|
+
chosen `upgrade_type` against each in parallel.
|
|
484
|
+
|
|
485
|
+
Form fields:
|
|
486
|
+
- `deployinfo` — JSON string with the testbed's device map. Shape:
|
|
487
|
+
`{"dev1":"fortinac01","ip1":"10.15.40.42","dev2":"ncm01","ip2":"10.15.40.43", ...}`
|
|
488
|
+
(same as `AutomationTBSerializer.deployinfo`)
|
|
489
|
+
- `upgrade_type` — `GA` / `build` / `file` / `command`
|
|
490
|
+
- `build_number` — when `upgrade_type=build`
|
|
491
|
+
- `file_uploaded` — when `upgrade_type=file`
|
|
492
|
+
- `command` — when `upgrade_type=command`
|
|
493
|
+
|
|
494
|
+
Response:
|
|
495
|
+
```json
|
|
496
|
+
{"upgrade_type": "build"}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
To monitor progress, poll `check-upgrade-status/` per-IP for each NAC
|
|
500
|
+
in the testbed.
|
|
501
|
+
|
|
502
|
+
Backed by: `nacviews.nac_upgrade_testbed`
|
|
503
|
+
|
|
504
|
+
### Diagnostic
|
|
505
|
+
|
|
506
|
+
#### `POST /adc/test_thread_db/`
|
|
507
|
+
|
|
508
|
+
Diagnostic helper that exercises threaded DB writes against the
|
|
509
|
+
`SingleDevice` model — used to verify upgrade-worker threading on a
|
|
510
|
+
host. Not used by the UI; do not call in production.
|
|
511
|
+
|
|
512
|
+
Backed by: `nacupgradeview.test_thread_db`
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## Other related APIs the `/automation` workflow touches
|
|
517
|
+
|
|
518
|
+
These belong to other pages (Shared Lab, Automation Testbed) but are
|
|
519
|
+
part of the same domain. Listed here so the dev team can find them
|
|
520
|
+
without spelunking.
|
|
521
|
+
|
|
522
|
+
### Automation testbed management — `adc.views.automation.automation_tbview`
|
|
523
|
+
|
|
524
|
+
| Endpoint | Method | Purpose |
|
|
525
|
+
|---|---|---|
|
|
526
|
+
| `/adc/create_automation_testbed/` | POST | Create a new automation testbed entry |
|
|
527
|
+
| `/adc/automationtb-list/` | POST | List testbeds by `category` + `keyword` filter |
|
|
528
|
+
| `/adc/automationtb-moduletitle/` | GET | Distinct module titles across testbeds (dropdown source) |
|
|
529
|
+
| `/adc/automationtb-update/` | POST | Update a testbed's metadata, `deployinfo`, or `new_name` |
|
|
530
|
+
|
|
531
|
+
### Performance lab (parallel of automation, used by `/performance`)
|
|
532
|
+
|
|
533
|
+
Mirror endpoints exist for performance-test workflows; they share the
|
|
534
|
+
same shape as the automation ones above. Listed for completeness in
|
|
535
|
+
case the dev team needs to script performance runs the same way.
|
|
536
|
+
|
|
537
|
+
| Endpoint | Method | Mirrors |
|
|
538
|
+
|---|---|---|
|
|
539
|
+
| `/adc/get_perf_testcases/` | GET | `get_testcases` |
|
|
540
|
+
| `/adc/get_perf_testcases_module/` | GET | (module-level filter) |
|
|
541
|
+
| `/adc/perf_robot_run/` | POST | `pytest_run` (Robot Framework instead of pytest) |
|
|
542
|
+
| `/adc/get_perf_lab/` | POST | `get_automation_lab` |
|
|
543
|
+
| `/adc/get_perf_test_result/` | POST | `get_test_result` |
|
|
544
|
+
| `/adc/get_perf_test_execution_by_id/` | POST | `get_test_execution_by_id` |
|
|
545
|
+
| `/adc/performance_lock/` | POST | `automation_lock` |
|
|
546
|
+
| `/adc/perf_exec_kill/` | POST | `test_exec_kill` |
|
|
547
|
+
| `/adc/perf_exec_delete/` | POST | `test_exec_delete` |
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Quick smoke test from the command line
|
|
552
|
+
|
|
553
|
+
```bash
|
|
554
|
+
T=$(curl -s -X POST http://10.15.33.11:8000/token-auth/ \
|
|
555
|
+
-H 'Content-Type: application/json' \
|
|
556
|
+
-d '{"username":"admin","password":"<your-pw>"}' | jq -r .token)
|
|
557
|
+
|
|
558
|
+
# 1. Pull versions
|
|
559
|
+
curl -s -H "Authorization: JWT $T" \
|
|
560
|
+
http://10.15.33.11:8000/adc/automation-verion/ | jq .
|
|
561
|
+
|
|
562
|
+
# 2. List your testbeds
|
|
563
|
+
curl -s -X POST -H "Authorization: JWT $T" \
|
|
564
|
+
-H 'Content-Type: application/json' \
|
|
565
|
+
-d '{"user":"admin"}' \
|
|
566
|
+
http://10.15.33.11:8000/adc/get_automation_lab | jq '.[] | {name, status}'
|
|
567
|
+
|
|
568
|
+
# 3. Fire a pytest run
|
|
569
|
+
EXEC_ID=$(curl -s -X POST -H "Authorization: JWT $T" \
|
|
570
|
+
-H 'Content-Type: application/json' \
|
|
571
|
+
-d '{
|
|
572
|
+
"user":"admin",
|
|
573
|
+
"lab":"L2Mode_7",
|
|
574
|
+
"testcase":["tests/L2/test_l2_radius.py::test_basic_auth"],
|
|
575
|
+
"argument":"-vv"
|
|
576
|
+
}' \
|
|
577
|
+
http://10.15.33.11:8000/adc/pytest_run | jq -r .exec_id)
|
|
578
|
+
echo "Started exec $EXEC_ID"
|
|
579
|
+
|
|
580
|
+
# 4. Poll progress
|
|
581
|
+
curl -s -X POST -H "Authorization: JWT $T" \
|
|
582
|
+
-H 'Content-Type: application/json' \
|
|
583
|
+
-d "{\"id\":$EXEC_ID}" \
|
|
584
|
+
http://10.15.33.11:8000/adc/get_test_execution_by_id | jq .status,.progress
|
|
585
|
+
|
|
586
|
+
# 5. Upgrade a NAC device to a specific build (out-of-band)
|
|
587
|
+
curl -s -X POST -H "Authorization: JWT $T" \
|
|
588
|
+
-F "ip=10.15.40.42" \
|
|
589
|
+
-F "upgrade_type=build" \
|
|
590
|
+
-F "build_number=7.6.6.0123" \
|
|
591
|
+
http://10.15.33.11:8000/adc/nac-upgrade/
|
|
592
|
+
|
|
593
|
+
# 6. Poll upgrade status
|
|
594
|
+
curl -s -X POST -H "Authorization: JWT $T" \
|
|
595
|
+
-H 'Content-Type: application/json' \
|
|
596
|
+
-d '{"ip":"10.15.40.42"}' \
|
|
597
|
+
http://10.15.33.11:8000/adc/check-upgrade-status/ | jq .
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
## Source files
|
|
603
|
+
|
|
604
|
+
| Concern | File |
|
|
605
|
+
|---|---|
|
|
606
|
+
| URL routes | `backend/adc/urls.py` |
|
|
607
|
+
| Run / lab / lock | `backend/adc/views/automation/automationview.py` |
|
|
608
|
+
| Testbed management | `backend/adc/views/automation/automation_tbview.py` |
|
|
609
|
+
| FortiNAC upgrade (single device + testbed) | `backend/adc/views/sharedlab/nacviews.py` |
|
|
610
|
+
| Upgrade with snapshot / status polling | `backend/adc/views/sharedlab/nacupgradeview.py` |
|
|
611
|
+
| Status widget | `backend/adc/views/sharedlab/tbviews.py` |
|
|
612
|
+
| Version dropdown | `backend/adc/views/dashboard/dashboardviews.py` |
|
|
613
|
+
| Frontend page | `frontend/src/pages/Automation/Automation.jsx` |
|
|
614
|
+
|
|
615
|
+
When adding a new endpoint, follow the pattern: route in
|
|
616
|
+
`adc/urls.py` → handler in the appropriate view module → serializer
|
|
617
|
+
if returning a model → consumer in `Automation.jsx`.
|
package/lib/agents/index.ts
CHANGED
|
@@ -240,7 +240,7 @@ export interface TerminalLaunchInfo {
|
|
|
240
240
|
model?: string; // profile model override (--model flag)
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
export function resolveTerminalLaunch(agentId?: string): TerminalLaunchInfo {
|
|
243
|
+
export function resolveTerminalLaunch(agentId?: string, scene: 'terminal' | 'task' | 'telegram' | 'help' | 'mobile' = 'terminal'): TerminalLaunchInfo {
|
|
244
244
|
const settings = loadSettings();
|
|
245
245
|
const agentCfg = settings.agents?.[agentId || 'claude'] || {};
|
|
246
246
|
// Resolve cliType: own cliType → base agent's cliType → base agent name guessing → agentId name guessing
|
|
@@ -263,18 +263,19 @@ export function resolveTerminalLaunch(agentId?: string): TerminalLaunchInfo {
|
|
|
263
263
|
|| undefined;
|
|
264
264
|
|
|
265
265
|
// Resolve env/model from this agent's per-use models sub-table.
|
|
266
|
-
// 顶层 model 字段已在 0.10 起被 migrate.ts
|
|
266
|
+
// 顶层 model 字段已在 0.10 起被 migrate.ts 清理,这里按 scene 读 models[scene]
|
|
267
|
+
// (默认 terminal;Help 传 'help' 等)。models[scene] === 'default' → 不加 --model。
|
|
267
268
|
let env: Record<string, string> | undefined;
|
|
268
269
|
let model: string | undefined;
|
|
269
270
|
if (agentCfg.base || agentCfg.env || agentCfg.models) {
|
|
270
271
|
if (agentCfg.env) env = { ...agentCfg.env };
|
|
271
|
-
model = agentCfg.models?.
|
|
272
|
+
model = agentCfg.models?.[scene];
|
|
272
273
|
if (model === 'default') model = undefined; // 'default' = 不加 --model
|
|
273
274
|
} else if (agentCfg.profile) {
|
|
274
275
|
const profileCfg = settings.agents?.[agentCfg.profile];
|
|
275
276
|
if (profileCfg) {
|
|
276
277
|
if (profileCfg.env) env = { ...profileCfg.env };
|
|
277
|
-
model = profileCfg.models?.
|
|
278
|
+
model = profileCfg.models?.[scene];
|
|
278
279
|
if (model === 'default') model = undefined;
|
|
279
280
|
}
|
|
280
281
|
}
|
package/lib/agents/migrate.ts
CHANGED
|
@@ -89,6 +89,14 @@ export function migrateAgentsFlatten(settings: Settings): boolean {
|
|
|
89
89
|
delete raw.model;
|
|
90
90
|
mutated = true;
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
// ── interactiveCmd 字段清理 ──────────────────────────
|
|
94
|
+
// 终端启动统一走 resolveTerminalLaunch(cliCmd = agentCfg.path),
|
|
95
|
+
// interactiveCmd 从未被后端读取,已从 UI / 类型移除。清掉残留死数据。
|
|
96
|
+
if (raw.interactiveCmd !== undefined) {
|
|
97
|
+
delete raw.interactiveCmd;
|
|
98
|
+
mutated = true;
|
|
99
|
+
}
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
// === 兜底校验 defaultAgent / chatAgent ===
|