@namewta/speculo 1.0.20 → 1.0.21
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/package.json +1 -1
- package/template/workflows/ops/D-project-deploy/D-project-deploy.md +1 -1
- package/template/workflows/ops/H-host-manage/H-host-manage.md +4 -0
- package/template/workflows/ops/H-host-manage/references/host-profiles.md +4 -0
- package/template/workflows/ops/I-initialize/I-initialize.md +1 -1
- package/template/workflows/ops/INDEX.md +2 -0
- package/template/workflows/ops/README.md +7 -3
- package/template/workflows/ops/S-server-connect/S-server-connect.md +5 -5
- package/template/workflows/ops/V-inventory-view/V-inventory-view.md +2 -0
- package/template/workflows/ops/common/CAPABILITIES.md +2 -0
- package/template/workflows/ops/common/USAGE.md +5 -5
- package/template/workflows/ops/common/WORKSPACE-USAGE.md +29 -2
- package/template/workflows/ops/common/examples/README.md +1 -1
- package/template/workflows/ops/common/examples/register.example.json +7 -3
- package/template/workflows/ops/common/examples/server-connection.example.json +11 -0
- package/template/workflows/ops/common/rules/deployment-layout.md +1 -1
- package/template/workflows/ops/common/rules/host-root-and-onboarding.md +47 -0
- package/template/workflows/ops/common/rules/persistence-and-secrets.md +1 -1
- package/template/workflows/ops/common/rules/research-and-decisions.md +8 -0
- package/template/workflows/ops/common/rules/server-onboarding.md +3 -3
- package/template/workflows/ops/common/rules/workspace-and-authorization.md +2 -2
- package/template/workflows/ops/common/schemas/fleet-view.schema.json +64 -0
- package/template/workflows/ops/common/schemas/host.schema.json +19 -0
- package/template/workflows/ops/common/schemas/plan.schema.json +57 -0
- package/template/workflows/ops/common/schemas/root-confirmation.schema.json +118 -0
- package/template/workflows/ops/common/schemas/server-connection.schema.json +67 -0
- package/template/workflows/ops/common/schemas/server-discovery.schema.json +171 -0
- package/template/workflows/ops/common/schemas/spec.schema.json +19 -0
- package/template/workflows/ops/common/schemas/status.schema.json +19 -0
- package/template/workflows/ops/common/templates/FLEET.html +1 -1
- package/template/workflows/ops/common/templates/HOST-README.md +3 -1
- package/template/workflows/ops/common/tests/test_ops.mjs +3 -3
- package/template/workflows/ops/common/tests/test_ops_bootstrap.mjs +30 -2
- package/template/workflows/ops/common/tests/test_ops_onboarding.mjs +225 -0
- package/template/workflows/ops/common/tools/demo-local.mjs +4 -0
- package/template/workflows/ops/common/tools/opslib/agent.mjs +26 -0
- package/template/workflows/ops/common/tools/opslib/bootstrap.mjs +50 -24
- package/template/workflows/ops/common/tools/opslib/cli.mjs +1 -0
- package/template/workflows/ops/common/tools/opslib/core.mjs +1 -1
- package/template/workflows/ops/common/tools/opslib/docs.mjs +1 -0
- package/template/workflows/ops/common/tools/opslib/execution.mjs +4 -0
- package/template/workflows/ops/common/tools/opslib/fleet.mjs +26 -4
- package/template/workflows/ops/common/tools/opslib/host_recipes.mjs +34 -0
- package/template/workflows/ops/common/tools/opslib/model.mjs +6 -0
- package/template/workflows/ops/common/tools/opslib/onboarding.mjs +215 -0
- package/template/workflows/ops/common/tools/opslib/planner.mjs +3 -0
- package/template/workflows/ops/common/tools/opslib/transport.mjs +7 -3
- package/template/workflows/ops/common/tools/opslib/workspace_cli.mjs +20 -1
- package/template/workflows/ops/common/tools/server-discover.sh +65 -0
- package/template/workflows/ops/common/tools/validate-ops.mjs +1 -1
- package/template/workflows/ops/manifest.json +12 -5
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "urn:speculo:ops:root-confirmation:v1",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"schema_version": {
|
|
7
|
+
"const": 1
|
|
8
|
+
},
|
|
9
|
+
"artifact": {
|
|
10
|
+
"const": "ops-root-confirmation"
|
|
11
|
+
},
|
|
12
|
+
"receipt_id": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
15
|
+
"maxLength": 80
|
|
16
|
+
},
|
|
17
|
+
"host_id": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
20
|
+
"maxLength": 80
|
|
21
|
+
},
|
|
22
|
+
"platform": {
|
|
23
|
+
"enum": [
|
|
24
|
+
"linux",
|
|
25
|
+
"windows",
|
|
26
|
+
"darwin"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"transport": {
|
|
30
|
+
"enum": [
|
|
31
|
+
"local",
|
|
32
|
+
"ssh"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"identity": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
38
|
+
},
|
|
39
|
+
"root": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"minLength": 1
|
|
42
|
+
},
|
|
43
|
+
"account": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": {
|
|
46
|
+
"username": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"minLength": 1
|
|
49
|
+
},
|
|
50
|
+
"uid": {
|
|
51
|
+
"type": "string"
|
|
52
|
+
},
|
|
53
|
+
"home": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"minLength": 1
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"required": [
|
|
59
|
+
"username",
|
|
60
|
+
"uid",
|
|
61
|
+
"home"
|
|
62
|
+
],
|
|
63
|
+
"additionalProperties": false
|
|
64
|
+
},
|
|
65
|
+
"endpoint_digest": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
68
|
+
},
|
|
69
|
+
"discovery": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"properties": {
|
|
72
|
+
"receipt_id": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
75
|
+
"maxLength": 80
|
|
76
|
+
},
|
|
77
|
+
"digest": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"required": [
|
|
83
|
+
"receipt_id",
|
|
84
|
+
"digest"
|
|
85
|
+
],
|
|
86
|
+
"additionalProperties": false
|
|
87
|
+
},
|
|
88
|
+
"confirmed_at": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"minLength": 1
|
|
91
|
+
},
|
|
92
|
+
"by": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"minLength": 1
|
|
95
|
+
},
|
|
96
|
+
"statement": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"minLength": 1
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"required": [
|
|
102
|
+
"schema_version",
|
|
103
|
+
"artifact",
|
|
104
|
+
"receipt_id",
|
|
105
|
+
"host_id",
|
|
106
|
+
"platform",
|
|
107
|
+
"transport",
|
|
108
|
+
"identity",
|
|
109
|
+
"root",
|
|
110
|
+
"account",
|
|
111
|
+
"endpoint_digest",
|
|
112
|
+
"discovery",
|
|
113
|
+
"confirmed_at",
|
|
114
|
+
"by",
|
|
115
|
+
"statement"
|
|
116
|
+
],
|
|
117
|
+
"additionalProperties": false
|
|
118
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "urn:speculo:ops:server-connection:v1",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"platform": {
|
|
7
|
+
"enum": [
|
|
8
|
+
"linux",
|
|
9
|
+
"windows",
|
|
10
|
+
"darwin"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"transport": {
|
|
14
|
+
"enum": [
|
|
15
|
+
"local",
|
|
16
|
+
"ssh"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"connection": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"hostname": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"minLength": 1
|
|
25
|
+
},
|
|
26
|
+
"username": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"minLength": 1
|
|
29
|
+
},
|
|
30
|
+
"port": {
|
|
31
|
+
"type": "integer",
|
|
32
|
+
"minimum": 1,
|
|
33
|
+
"maximum": 65535
|
|
34
|
+
},
|
|
35
|
+
"known_hosts": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"minLength": 1
|
|
38
|
+
},
|
|
39
|
+
"identity_file": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"minLength": 1
|
|
42
|
+
},
|
|
43
|
+
"node": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"minLength": 1
|
|
46
|
+
},
|
|
47
|
+
"sudo": {
|
|
48
|
+
"type": "boolean"
|
|
49
|
+
},
|
|
50
|
+
"shell": {
|
|
51
|
+
"enum": [
|
|
52
|
+
"posix",
|
|
53
|
+
"powershell"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"required": [],
|
|
58
|
+
"additionalProperties": false
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"required": [
|
|
62
|
+
"platform",
|
|
63
|
+
"transport",
|
|
64
|
+
"connection"
|
|
65
|
+
],
|
|
66
|
+
"additionalProperties": false
|
|
67
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "urn:speculo:ops:server-discovery:v1",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"schema_version": {
|
|
7
|
+
"const": 1
|
|
8
|
+
},
|
|
9
|
+
"artifact": {
|
|
10
|
+
"const": "ops-server-discovery"
|
|
11
|
+
},
|
|
12
|
+
"discovery_id": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
15
|
+
"maxLength": 80
|
|
16
|
+
},
|
|
17
|
+
"host_id": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
20
|
+
"maxLength": 80
|
|
21
|
+
},
|
|
22
|
+
"discovered_at": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"minLength": 1
|
|
25
|
+
},
|
|
26
|
+
"endpoint": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"host_id": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
32
|
+
"maxLength": 80
|
|
33
|
+
},
|
|
34
|
+
"platform": {
|
|
35
|
+
"enum": [
|
|
36
|
+
"linux",
|
|
37
|
+
"windows",
|
|
38
|
+
"darwin"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"transport": {
|
|
42
|
+
"enum": [
|
|
43
|
+
"local",
|
|
44
|
+
"ssh"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"connection": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"properties": {
|
|
50
|
+
"hostname": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"minLength": 1
|
|
53
|
+
},
|
|
54
|
+
"username": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"minLength": 1
|
|
57
|
+
},
|
|
58
|
+
"port": {
|
|
59
|
+
"type": "integer",
|
|
60
|
+
"minimum": 1,
|
|
61
|
+
"maximum": 65535
|
|
62
|
+
},
|
|
63
|
+
"known_hosts": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"minLength": 1
|
|
66
|
+
},
|
|
67
|
+
"identity_file": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"minLength": 1
|
|
70
|
+
},
|
|
71
|
+
"node": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"minLength": 1
|
|
74
|
+
},
|
|
75
|
+
"sudo": {
|
|
76
|
+
"type": "boolean"
|
|
77
|
+
},
|
|
78
|
+
"shell": {
|
|
79
|
+
"enum": [
|
|
80
|
+
"posix",
|
|
81
|
+
"powershell"
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"required": [],
|
|
86
|
+
"additionalProperties": false
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"required": [
|
|
90
|
+
"host_id",
|
|
91
|
+
"platform",
|
|
92
|
+
"transport",
|
|
93
|
+
"connection"
|
|
94
|
+
],
|
|
95
|
+
"additionalProperties": false
|
|
96
|
+
},
|
|
97
|
+
"endpoint_digest": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
100
|
+
},
|
|
101
|
+
"facts": {
|
|
102
|
+
"type": "object",
|
|
103
|
+
"properties": {
|
|
104
|
+
"identity": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
107
|
+
},
|
|
108
|
+
"platform": {
|
|
109
|
+
"enum": [
|
|
110
|
+
"linux",
|
|
111
|
+
"windows",
|
|
112
|
+
"darwin"
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
"username": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"minLength": 1
|
|
118
|
+
},
|
|
119
|
+
"uid": {
|
|
120
|
+
"type": "string"
|
|
121
|
+
},
|
|
122
|
+
"home": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"minLength": 1
|
|
125
|
+
},
|
|
126
|
+
"node": {
|
|
127
|
+
"type": [
|
|
128
|
+
"string",
|
|
129
|
+
"null"
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
"node_version": {
|
|
133
|
+
"type": [
|
|
134
|
+
"string",
|
|
135
|
+
"null"
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"required": [
|
|
140
|
+
"identity",
|
|
141
|
+
"platform",
|
|
142
|
+
"username",
|
|
143
|
+
"uid",
|
|
144
|
+
"home",
|
|
145
|
+
"node",
|
|
146
|
+
"node_version"
|
|
147
|
+
],
|
|
148
|
+
"additionalProperties": false
|
|
149
|
+
},
|
|
150
|
+
"suggested_root": {
|
|
151
|
+
"type": "string",
|
|
152
|
+
"minLength": 1
|
|
153
|
+
},
|
|
154
|
+
"registered": {
|
|
155
|
+
"type": "boolean"
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
"required": [
|
|
159
|
+
"schema_version",
|
|
160
|
+
"artifact",
|
|
161
|
+
"discovery_id",
|
|
162
|
+
"host_id",
|
|
163
|
+
"discovered_at",
|
|
164
|
+
"endpoint",
|
|
165
|
+
"endpoint_digest",
|
|
166
|
+
"facts",
|
|
167
|
+
"suggested_root",
|
|
168
|
+
"registered"
|
|
169
|
+
],
|
|
170
|
+
"additionalProperties": false
|
|
171
|
+
}
|
|
@@ -160,6 +160,25 @@
|
|
|
160
160
|
],
|
|
161
161
|
"additionalProperties": false
|
|
162
162
|
}
|
|
163
|
+
},
|
|
164
|
+
"root_confirmation": {
|
|
165
|
+
"type": "object",
|
|
166
|
+
"properties": {
|
|
167
|
+
"receipt_id": {
|
|
168
|
+
"type": "string",
|
|
169
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
170
|
+
"maxLength": 80
|
|
171
|
+
},
|
|
172
|
+
"digest": {
|
|
173
|
+
"type": "string",
|
|
174
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
"required": [
|
|
178
|
+
"receipt_id",
|
|
179
|
+
"digest"
|
|
180
|
+
],
|
|
181
|
+
"additionalProperties": false
|
|
163
182
|
}
|
|
164
183
|
},
|
|
165
184
|
"required": [
|
|
@@ -362,6 +362,25 @@
|
|
|
362
362
|
],
|
|
363
363
|
"additionalProperties": false
|
|
364
364
|
}
|
|
365
|
+
},
|
|
366
|
+
"root_confirmation": {
|
|
367
|
+
"type": "object",
|
|
368
|
+
"properties": {
|
|
369
|
+
"receipt_id": {
|
|
370
|
+
"type": "string",
|
|
371
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
372
|
+
"maxLength": 80
|
|
373
|
+
},
|
|
374
|
+
"digest": {
|
|
375
|
+
"type": "string",
|
|
376
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
"required": [
|
|
380
|
+
"receipt_id",
|
|
381
|
+
"digest"
|
|
382
|
+
],
|
|
383
|
+
"additionalProperties": false
|
|
365
384
|
}
|
|
366
385
|
},
|
|
367
386
|
"required": [
|
|
@@ -47,7 +47,7 @@ function render(){const server=$("server").value,project=$("project").value,st=$
|
|
|
47
47
|
const ds=data.deployments.filter(d=>(!server||d.server_id===server)&&(!project||d.project_id===project)&&(!st||d.status===st)&&match(d));
|
|
48
48
|
const ss=data.servers.filter(s=>(!server||s.server_id===server)&&(!project||s.deployment_ids.some(id=>byDeployment.get(id)?.project_id===project))&&(!st||ds.some(d=>d.server_id===s.server_id))&&(match(s)||ds.some(d=>d.server_id===s.server_id)));
|
|
49
49
|
const ps=data.projects.filter(p=>(!project||p.project_id===project)&&(!server||p.deployment_ids.some(id=>byDeployment.get(id)?.server_id===server))&&(!st||ds.some(d=>d.project_id===p.project_id))&&(match(p)||ds.some(d=>d.project_id===p.project_id)));
|
|
50
|
-
table("servers",["服务器 / 名称","地址 / 系统","检查结果","检查详情(* 必需项)","项目","
|
|
50
|
+
table("servers",["服务器 / 名称","地址 / 系统","检查结果","检查详情(* 必需项)","项目","账号 / 持久化根 / 确认 / 文档"],ss.map(s=>[links("server",[s.server_id]),`${s.name} · ${s.address}${s.port?":"+s.port:""} / ${s.platform}`,badge(s.readiness),checks(s),links("project",s.project_ids),`${s.login_account} / ${s.root} / ${s.root_confirmation.status} / ${s.initialization.documents}`]));
|
|
51
51
|
table("projects",["项目","名称","类别 / 服务类型","所在服务器","部署数量"],ps.map(p=>[link("project",p.project_id),p.name,`${p.kind} / ${p.service_type||"—"}`,links("server",p.server_ids),p.deployment_ids.length]));
|
|
52
52
|
table("deployments",["部署实例","项目 / 服务器","环境 / 方法","记录状态","计划版本","验证版本","根目录 / 更新时间"],ds.map(d=>{const where=node("div");where.append(link("project",d.project_id),node("div"),link("server",d.server_id));return[d.deployment_id,where,`${d.environment} / ${d.instance} / ${d.method}`,badge(d.status),d.desired_version,d.observed_version,`${d.root} / ${d.updated_at||"未验证"}`];}));
|
|
53
53
|
table("services",["服务器","主机级服务","类型","systemd 单元","监听端口"],ss.flatMap(s=>s.host_services.map(v=>[link("server",s.server_id),v.service_id,v.kind,v.unit,v.port])));
|
|
@@ -67,7 +67,7 @@ function setupFixture() {
|
|
|
67
67
|
project_id: "app-a", display_name: "APP A", kind: "app", service_type: null,
|
|
68
68
|
source: { type: "local", location: join(tmp, "source"), revision: "v1" },
|
|
69
69
|
};
|
|
70
|
-
model.
|
|
70
|
+
const legacy = model.load(state); legacy.hosts[host.host_id] = host; legacy.projects[project.project_id] = project; model.save(state, legacy); // pre-upgrade v3 fixture
|
|
71
71
|
model.putCredential(state, { credential_id: "app-auth", version: 1, purpose: "Synthetic test only", values: { username: "app-user", password: PASSWORD } });
|
|
72
72
|
const spec = (version = "v1", operation = "deploy", program = null) => {
|
|
73
73
|
if (program == null) {
|
|
@@ -440,7 +440,7 @@ test("planner contracts", async (t) => {
|
|
|
440
440
|
});
|
|
441
441
|
await each("unrelated host registration does not block approval", (fx) => {
|
|
442
442
|
const [info] = fx.plan();
|
|
443
|
-
model.
|
|
443
|
+
const legacy = model.load(fx.state); legacy.hosts["node-b"] = { ...fx.host, host_id: "node-b", display_name: "Node B", root: join(fx.tmp, "server-b") }; model.save(fx.state, legacy); // preserve legacy multi-root executor coverage
|
|
444
444
|
fx.approve(info);
|
|
445
445
|
});
|
|
446
446
|
await each("touched host catalog drift blocks approval", (fx) => {
|
|
@@ -888,7 +888,7 @@ test("control file helpers", () => {
|
|
|
888
888
|
});
|
|
889
889
|
|
|
890
890
|
test("disjoint host plans apply sequentially", () => runFx((fx) => {
|
|
891
|
-
model.
|
|
891
|
+
const legacy = model.load(fx.state); legacy.hosts["node-b"] = { ...fx.host, host_id: "node-b", display_name: "Node B", root: join(fx.tmp, "server-b") }; model.save(fx.state, legacy); // preserve legacy multi-root executor coverage
|
|
892
892
|
const specA = {
|
|
893
893
|
schema_version: 1, worker: "H", operation: "maintain", hosts: ["node-a"], reason: "Host A cache fixture",
|
|
894
894
|
rollback_note: "Keep isolated cache until a separate purge is approved.",
|
|
@@ -14,6 +14,7 @@ import * as bootstrap from "../tools/opslib/bootstrap.mjs";
|
|
|
14
14
|
import { initialize, main, cliHooks } from "../tools/opslib/cli.mjs";
|
|
15
15
|
import * as agent from "../tools/opslib/agent.mjs";
|
|
16
16
|
import * as model from "../tools/opslib/model.mjs";
|
|
17
|
+
import { onboardingHooks, discoverServer, confirmServerRoot } from "../tools/opslib/onboarding.mjs";
|
|
17
18
|
|
|
18
19
|
const toolsDir = join(dirname(fileURLToPath(import.meta.url)), "../tools");
|
|
19
20
|
const scriptPath = join(toolsDir, "bootstrap-volta.sh");
|
|
@@ -258,9 +259,13 @@ function cliFixture({ executeLinux = false } = {}) {
|
|
|
258
259
|
const archives = makeArchives(tmp);
|
|
259
260
|
const prevPin = bootstrap.bootstrapHooks.loadPin;
|
|
260
261
|
bootstrap.bootstrapHooks.loadPin = () => pinFor(archives);
|
|
262
|
+
const previousObserve = onboardingHooks.observe;
|
|
263
|
+
onboardingHooks.observe = (h, root) => ({ identity: executeLinux ? agent.fingerprint() : "a".repeat(64), platform: "linux", username: h.connection.username, uid: "1000", home: "/home/ops", node: null, node_version: null, ...(root ? {root_check: {exists: false, writable: true, nonempty: false, owner: null}} : {}) });
|
|
261
264
|
return {
|
|
262
265
|
tmp, state, target, kh, archives,
|
|
266
|
+
confirm(host) { const d = discoverServer(state, host.host_id, host); const r = confirmServerRoot(state, host.host_id, d.discovery.receipt_id, host.root, "fixture-user", "Confirm only this isolated simulated target root"); host.root_confirmation = r.root_confirmation; },
|
|
263
267
|
cleanup() {
|
|
268
|
+
onboardingHooks.observe = previousObserve;
|
|
264
269
|
bootstrap.bootstrapHooks.loadPin = prevPin;
|
|
265
270
|
rmSync(tmp, { recursive: true, force: true });
|
|
266
271
|
},
|
|
@@ -279,6 +284,7 @@ test("Linux SSH bootstrap and enrollment preserve remote paths on every controll
|
|
|
279
284
|
const mock = mockTransport({ probeJson: missingProbe, applyJson: { status: "installed", connection_node: connectionNode, profile_unchanged: true } });
|
|
280
285
|
try {
|
|
281
286
|
const host = sshHost({ root: fx.target, kh: fx.kh, node: "node" });
|
|
287
|
+
fx.confirm(host);
|
|
282
288
|
const conn = writeConn(fx.tmp, host);
|
|
283
289
|
const r = captureMain(["bootstrap-node", "--state", fx.state, "--host-id", host.host_id, "--connection-file", conn, "--apply", "--account", "ops", "--host-root", fx.target, "--ack", ACK,
|
|
284
290
|
"--volta-archive", fx.archives.voltaTar, "--volta-sha256", fx.archives.voltaSha, "--node-archive", fx.archives.nodeTar, "--node-sha256", fx.archives.nodeSha]);
|
|
@@ -290,6 +296,15 @@ test("Linux SSH bootstrap and enrollment preserve remote paths on every controll
|
|
|
290
296
|
assert.deepEqual(applyArgs.slice(applyArgs.indexOf("--apply") + 1), [paths[0], fx.archives.voltaSha, paths[1], fx.archives.nodeSha, posix.dirname(posix.dirname(connectionNode)), fx.archives.nodeVersion, ACK]);
|
|
291
297
|
assert.equal(JSON.parse(readFileSync(boot.receipt, "utf8")).connection_node, connectionNode);
|
|
292
298
|
assert.deepEqual(Object.keys(model.load(fx.state).hosts), []);
|
|
299
|
+
// A new confirmation cannot reset an interrupted bootstrap's machine/root guard.
|
|
300
|
+
fx.confirm(host); writeConn(fx.tmp, host);
|
|
301
|
+
const retryArgs = ["bootstrap-node", "--state", fx.state, "--host-id", host.host_id, "--connection-file", conn, "--apply", "--account", "ops", "--host-root", fx.target, "--ack", ACK,
|
|
302
|
+
"--volta-archive", fx.archives.voltaTar, "--node-archive", fx.archives.nodeTar];
|
|
303
|
+
const blockedRetry = captureMain(retryArgs); assert.equal(blockedRetry.code, 2); assert.match(blockedRetry.stderr, /already started/);
|
|
304
|
+
assert.equal(mock.log.filter(e => e.cmd === "scp").length, 2);
|
|
305
|
+
const recovered = captureMain([...retryArgs, "--recover-ack", "I-VERIFIED-BOOTSTRAP-IS-STOPPED", "--recovery-statement", "Fixture only: the simulated old process is stopped; retry exactly the same pinned inputs"]);
|
|
306
|
+
assert.equal(recovered.code, 0, recovered.stderr);
|
|
307
|
+
assert.ok(readdirSync(join(fx.state, "hosts", host.host_id, "bootstrap")).some(n => n.startsWith("recovery-")));
|
|
293
308
|
mock.restore();
|
|
294
309
|
host.connection.node = connectionNode;
|
|
295
310
|
const enrollFile = join(fx.tmp, "register.json"); writeFileSync(enrollFile, JSON.stringify({ hosts: [host], projects: [] }));
|
|
@@ -355,6 +370,18 @@ test("bootstrap-node apply without ack blocked", () => {
|
|
|
355
370
|
} finally { mock.restore(); fx.cleanup(); }
|
|
356
371
|
});
|
|
357
372
|
|
|
373
|
+
test("bootstrap with valid archives but no root confirmation cannot write or upload", () => {
|
|
374
|
+
const fx = cliFixture(), mock = mockTransport({ probeJson: missingProbe });
|
|
375
|
+
try {
|
|
376
|
+
const host = sshHost({ root: fx.target, kh: fx.kh, node: "node" }), conn = writeConn(fx.tmp, host);
|
|
377
|
+
const r = captureMain(["bootstrap-node", "--state", fx.state, "--host-id", host.host_id, "--connection-file", conn, "--apply", "--account", "ops", "--host-root", fx.target, "--ack", ACK,
|
|
378
|
+
"--volta-archive", fx.archives.voltaTar, "--node-archive", fx.archives.nodeTar]);
|
|
379
|
+
assert.equal(r.code, 2); assert.match(r.stderr, /root-confirm/);
|
|
380
|
+
assert.equal(mock.log.filter(e => e.cmd === "scp").length, 0);
|
|
381
|
+
assert.ok(mock.log.every(e => e.args.at(-1).includes("--probe")));
|
|
382
|
+
} finally { mock.restore(); fx.cleanup(); }
|
|
383
|
+
});
|
|
384
|
+
|
|
358
385
|
test("bootstrap-node sha mismatch blocked", () => {
|
|
359
386
|
const fx = cliFixture();
|
|
360
387
|
const mock = mockTransport({ probeJson: missingProbe });
|
|
@@ -428,6 +455,7 @@ test("bootstrap-node apply then enroll writes inventory and status", linuxExecut
|
|
|
428
455
|
const mock = mockTransport({ probeJson: missingProbe });
|
|
429
456
|
try {
|
|
430
457
|
const host = sshHost({ root: fx.target, kh: fx.kh, node: "node" });
|
|
458
|
+
fx.confirm(host);
|
|
431
459
|
const conn = writeConn(fx.tmp, host);
|
|
432
460
|
const applied = captureMain([
|
|
433
461
|
"bootstrap-node", "--state", fx.state, "--host-id", "wsl-a",
|
|
@@ -518,7 +546,7 @@ test("environmentSpec uses managed volta path", () => {
|
|
|
518
546
|
host_id: "node-a", display_name: "Node A", platform: process.platform === "win32" ? "windows" : "linux",
|
|
519
547
|
transport: "local", root: target, identity: agent.fingerprint(), connection: {},
|
|
520
548
|
};
|
|
521
|
-
model.
|
|
549
|
+
const legacy = model.load(state); legacy.hosts[host.host_id] = host; model.save(state, legacy); // existing runtime, no fabricated confirmation
|
|
522
550
|
const prev = recipes.hostRecipesHooks.call;
|
|
523
551
|
const voltaBin = core.targetJoin(host, "_host/toolchains/ops/volta/bin/volta");
|
|
524
552
|
recipes.hostRecipesHooks.call = (_h, req) => {
|
|
@@ -554,7 +582,7 @@ test("environmentSpec still throws when volta truly absent", () => {
|
|
|
554
582
|
host_id: "node-a", display_name: "Node A", platform: process.platform === "win32" ? "windows" : "linux",
|
|
555
583
|
transport: "local", root: target, identity: agent.fingerprint(), connection: {},
|
|
556
584
|
};
|
|
557
|
-
model.
|
|
585
|
+
const legacy = model.load(state); legacy.hosts[host.host_id] = host; model.save(state, legacy); // existing runtime, no fabricated confirmation
|
|
558
586
|
const prev = recipes.hostRecipesHooks.call;
|
|
559
587
|
recipes.hostRecipesHooks.call = (_h, req) => {
|
|
560
588
|
if (req.action === "snapshot") return { paths: { [req.paths[0]]: { kind: "absent" } } };
|