cs-image-system-bash-mod-plugin 0.1.6__tar.gz → 0.1.8__tar.gz
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.
- {cs_image_system_bash_mod_plugin-0.1.6 → cs_image_system_bash_mod_plugin-0.1.8}/.gitignore +7 -2
- {cs_image_system_bash_mod_plugin-0.1.6 → cs_image_system_bash_mod_plugin-0.1.8}/PKG-INFO +117 -139
- {cs_image_system_bash_mod_plugin-0.1.6 → cs_image_system_bash_mod_plugin-0.1.8}/README.md +114 -136
- {cs_image_system_bash_mod_plugin-0.1.6 → cs_image_system_bash_mod_plugin-0.1.8}/pyproject.toml +3 -3
- {cs_image_system_bash_mod_plugin-0.1.6 → cs_image_system_bash_mod_plugin-0.1.8}/src/cs_image_system/bash_mod_plugin/bash_builder.py +8 -10
- {cs_image_system_bash_mod_plugin-0.1.6 → cs_image_system_bash_mod_plugin-0.1.8}/src/cs_image_system/bash_mod_plugin/bash_models.py +10 -8
- {cs_image_system_bash_mod_plugin-0.1.6 → cs_image_system_bash_mod_plugin-0.1.8}/src/cs_image_system/bash_mod_plugin/main.py +4 -2
- {cs_image_system_bash_mod_plugin-0.1.6 → cs_image_system_bash_mod_plugin-0.1.8}/tests/test_bash_provisioner.py +11 -3
- {cs_image_system_bash_mod_plugin-0.1.6 → cs_image_system_bash_mod_plugin-0.1.8}/LICENSE +0 -0
- {cs_image_system_bash_mod_plugin-0.1.6 → cs_image_system_bash_mod_plugin-0.1.8}/src/cs_image_system/bash_mod_plugin/__init__.py +0 -0
- {cs_image_system_bash_mod_plugin-0.1.6 → cs_image_system_bash_mod_plugin-0.1.8}/src/cs_image_system/py.typed +0 -0
|
@@ -13,16 +13,21 @@ build/
|
|
|
13
13
|
.venv/
|
|
14
14
|
.DS_Store
|
|
15
15
|
packages/**/*.code-workspace
|
|
16
|
-
# the IDE rewrites the root workspace file whenever folders are opened: it is the editor's, not the repository's
|
|
16
|
+
# the IDE rewrites the root workspace file whenever folders are opened: it is the editor's, not the repository's
|
|
17
17
|
/*.code-workspace
|
|
18
18
|
trial
|
|
19
19
|
tfmodules/tftest
|
|
20
20
|
YAML_DUMP.yaml
|
|
21
|
+
# a configuration tree's run-local plaintexts and debug output, wherever a tree
|
|
22
|
+
# sits in this repository (a copy of the frozen fixture included)
|
|
23
|
+
_private/
|
|
21
24
|
.obsidian
|
|
22
25
|
.tofu-plugin-cache/
|
|
23
26
|
_uncommitted/
|
|
24
27
|
target/
|
|
25
|
-
# never publishable, whatever the root (
|
|
28
|
+
# never publishable, whatever the root (the public-safe hook refuses them; `just publish-tree` checks the policy)
|
|
26
29
|
tfplan
|
|
27
30
|
*.tfstate
|
|
28
31
|
*.tfstate.backup
|
|
32
|
+
REEXAMINE*
|
|
33
|
+
TODO2.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cs-image-system-bash-mod-plugin
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: The shell modification plugin: inline script, script files and an ensure form become Packer shell provisioners
|
|
5
5
|
Project-URL: Repository, https://github.com/infrastructurebuilder/cs-image-system-3
|
|
6
6
|
Project-URL: Documentation, https://github.com/infrastructurebuilder/cs-image-system-3/blob/develop/docs/OPERATIONS.md
|
|
@@ -9,8 +9,8 @@ Author-email: Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
|
9
9
|
License-Expression: Apache-2.0
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Python: >=3.13
|
|
12
|
-
Requires-Dist: cs-image-system-base==0.1.
|
|
13
|
-
Requires-Dist: cs-image-system-contract
|
|
12
|
+
Requires-Dist: cs-image-system-base==0.1.8
|
|
13
|
+
Requires-Dist: cs-image-system-contract<0.5,>=0.4
|
|
14
14
|
Requires-Dist: pydantic>=2.13
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
|
|
@@ -55,18 +55,15 @@ How a YAML entry selects these classes:
|
|
|
55
55
|
looks the builder up by name or alias, reads the builder's own `type`
|
|
56
56
|
(`bash-remote`) and loads the item into the class registered under that
|
|
57
57
|
service key for `MOD_BUILDER_ITEM_MODEL`: `BashModItemModel`. The
|
|
58
|
-
registry alone decides
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
empty `helpers.py`.
|
|
62
|
-
- **The item's `type` is rewritten to the builder's `name` at load**
|
|
63
|
-
(stage 63 item 15). The image builder later fetches the builder with
|
|
58
|
+
registry alone decides.
|
|
59
|
+
- **The item's `type` is rewritten to the builder's `name` at load.**
|
|
60
|
+
The image builder later fetches the builder with
|
|
64
61
|
`ctx.mod_builders.get(mod.get_type())`, a map keyed by builder NAME
|
|
65
|
-
only, so an alias must not survive the load
|
|
66
|
-
|
|
67
|
-
`type: bash-remote`. Before 2026-09-25 the alias was kept, and
|
|
68
|
-
generation stopped with
|
|
62
|
+
only, so an alias must not survive the load: a kept alias would stop
|
|
63
|
+
generation with
|
|
69
64
|
`AssertionError: No mod builder bash found in context for <image>`.
|
|
65
|
+
An item declared `type: bash` (the fixture builder's alias) is loaded
|
|
66
|
+
as `type: bash-remote`.
|
|
70
67
|
- An `executables:` entry named `bash` is version-checked by
|
|
71
68
|
`BashVersionChecker` (regex `.*version\s+([\d\.]+)` over
|
|
72
69
|
`bash --version`).
|
|
@@ -80,21 +77,31 @@ Defined in [bash_models.py](src/cs_image_system/bash_mod_plugin/bash_models.py).
|
|
|
80
77
|
[`ModBuilderModel`](../base/src/cs_image_system/base/models/mod_builder.py),
|
|
81
78
|
which adds nothing to
|
|
82
79
|
[`BuilderModel`](../base/src/cs_image_system/base/models/builder_model.py)
|
|
83
|
-
(`name`, `type`, `description`, `aliases`,
|
|
84
|
-
`config`, `gitignore`, `tags
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
80
|
+
(the common builder fields: `name`, `type`, `description`, `aliases`,
|
|
81
|
+
`executable`, `is_default`, `config`, `gitignore`, `tags`;
|
|
82
|
+
[CONFIGURATION section 4.1](../../docs/CONFIGURATION.md#41-fields-common-to-every-builder)
|
|
83
|
+
holds that table). `BashModBuilderModel` is the registered subclass; it
|
|
84
|
+
adds no fields.
|
|
85
|
+
|
|
86
|
+
The keys this plugin adds are `execute_command`, `configuration_user`,
|
|
87
|
+
`environment_vars` and `expect_disconnect`: each is a Packer
|
|
88
|
+
shell-provisioner argument, emitted on every shell provisioner of every
|
|
89
|
+
item of the builder.
|
|
90
|
+
[CONFIGURATION section 7.2](../../docs/CONFIGURATION.md#72-bash-remote)
|
|
91
|
+
holds the field table; [Configuration reference](#configuration-reference)
|
|
92
|
+
below gives this plugin's reading of each key. Rules only this plugin
|
|
93
|
+
enforces:
|
|
94
|
+
|
|
95
|
+
- `execute_command` (Packer's command template, for example
|
|
96
|
+
`sudo -E bash '{{.Path}}'`) is the way to change how the script is
|
|
97
|
+
invoked. `configuration_user` writes its own: every shell provisioner
|
|
98
|
+
of the builder gets `execute_command = "chmod +x {{ .Path }}; sudo su -
|
|
99
|
+
<user> -c '{{ .Vars }} {{ .Path }}'"`, so each item's lines run as that
|
|
100
|
+
user and the bake user only launches them. Because both say how the
|
|
101
|
+
script runs, declaring them together is refused at load.
|
|
102
|
+
- `extra_arguments` is not a field: a builder that declares it fails to
|
|
103
|
+
load with the unknown-key refusal. Use `execute_command` to change the
|
|
104
|
+
invocation.
|
|
98
105
|
|
|
99
106
|
Base fields this plugin constrains: `executable` should name the `bash`
|
|
100
107
|
executable (the fixture does), though the emission does not invoke it;
|
|
@@ -105,13 +112,28 @@ base-model rule). `config:` on the builder is carried but never read.
|
|
|
105
112
|
|
|
106
113
|
Same file. Extends
|
|
107
114
|
[`ModItemModel`](../base/src/cs_image_system/base/models/moditem_type.py)
|
|
108
|
-
(`SubRootItem` -> `RootItem` -> `NameTyped`).
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
(`SubRootItem` -> `RootItem` -> `NameTyped`). The item's own keys are
|
|
116
|
+
`script`, `scripts` and `ensure`; the inherited ones (`name`, `type`,
|
|
117
|
+
`description`, `aliases`, `tags`, `config`) are the modification item's,
|
|
118
|
+
tabled in
|
|
119
|
+
[CONFIGURATION section 7.3](../../docs/CONFIGURATION.md#73-the-modification-item-inside-an-images-modifications),
|
|
120
|
+
and [Configuration reference](#configuration-reference) below reads every
|
|
121
|
+
key as this plugin does.
|
|
122
|
+
|
|
123
|
+
- `script` is the list of inline command lines, run in order in one
|
|
124
|
+
`provisioner "shell"` (`inline = [...]`). Literal shell; no templating.
|
|
125
|
+
Blank entries are dropped.
|
|
126
|
+
- `scripts` is the list of script files, relative to the configuration
|
|
127
|
+
root (the process's working directory, which the configuration load
|
|
128
|
+
sets to the root), copied beside the Packer root and run as
|
|
129
|
+
`scripts = [...]`. Blank entries are dropped.
|
|
130
|
+
- `ensure` is the declarative form. Allowed keys: `packages` (list of
|
|
131
|
+
names), `files` (list of `{path, content, mode}`; `mode` defaults to
|
|
132
|
+
`0644`), `services` (list of unit names, enabled and started),
|
|
133
|
+
`commands` (list of `{run, unless}`; `run` executes only when `unless`
|
|
134
|
+
fails). Every entry is validated at load by `validate_ensure`: a wrong
|
|
135
|
+
shape, an unknown key, a bad mode or an all-empty mapping raises
|
|
136
|
+
`ValueError` naming the item and the entry.
|
|
115
137
|
|
|
116
138
|
Validation at load: at least one of `script`, `scripts`, `ensure` must be
|
|
117
139
|
non-empty, otherwise `ValueError` ("must supply 'script' (inline lines),
|
|
@@ -134,15 +156,13 @@ Derived behaviour:
|
|
|
134
156
|
- `remap_self_with_copied_assets(copied)`: replaces each `scripts` entry
|
|
135
157
|
with its copied path when present in the mapping.
|
|
136
158
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
| `config` | `RootItem` | `{}` | Not read by this plugin. It takes part in the lineage content hash of the modification and nothing else. |
|
|
145
|
-
| `_model_id` | `ModItemModel` | set at load | The owning image's global id; `identified_model` resolves to the image. |
|
|
159
|
+
Of the inherited fields, `name` names the provisioner comment and the
|
|
160
|
+
local bundle directory (`NN-<name>`); `type` is the foreign key to a
|
|
161
|
+
`mod_builders:` entry (see above); `description`, `aliases` and `tags`
|
|
162
|
+
are carried and not emitted; `config` is not read by this plugin and
|
|
163
|
+
takes part in the lineage content hash of the modification and nothing
|
|
164
|
+
else; `_model_id`, set at load, is the owning image's global id, and
|
|
165
|
+
`identified_model` resolves to the image.
|
|
146
166
|
|
|
147
167
|
### A modification item with only `config:`
|
|
148
168
|
|
|
@@ -171,10 +191,11 @@ calls, in order:
|
|
|
171
191
|
`scripts` to `<block_dir>/<script path>` and returns
|
|
172
192
|
`{script: script path}`. The source is resolved against the process's
|
|
173
193
|
current working directory; the configuration load has already changed
|
|
174
|
-
directory to the configuration root (`--root-dir`, else
|
|
175
|
-
`working_directory` in `cfg/_config.yml
|
|
176
|
-
path is root-relative. A missing file raises
|
|
177
|
-
absolute path is not copied and is left in
|
|
194
|
+
directory to the configuration root (`--root-dir`, else the directory
|
|
195
|
+
the command was started from; `working_directory` in `cfg/_config.yml`
|
|
196
|
+
is not read), so the path is root-relative. A missing file raises
|
|
197
|
+
`FileNotFoundError`; an absolute path is not copied and is left in
|
|
198
|
+
place.
|
|
178
199
|
2. `mod.remap_self_with_copied_assets(mapping)` on the item. Because the
|
|
179
200
|
mapping's value is the same relative path the copy was placed at, the
|
|
180
201
|
item's `scripts` entries keep their text; the emitted `scripts = [...]`
|
|
@@ -183,8 +204,8 @@ calls, in order:
|
|
|
183
204
|
4. `generate_items_during_modification(image, mod, image_builder, phase, build_path)`:
|
|
184
205
|
- `inline` = `ensure_lines()` followed by the `script` lines; `scripts`
|
|
185
206
|
= the script files. When both lists are empty it logs a warning and
|
|
186
|
-
emits nothing;
|
|
187
|
-
|
|
207
|
+
emits nothing; an all-empty `ensure` is refused at load, so an item
|
|
208
|
+
cannot reach this.
|
|
188
209
|
- Appends `# Modifications for <item name> of type <item type> (shell)`.
|
|
189
210
|
- Then, because Packer's shell provisioner takes either `scripts` or
|
|
190
211
|
`inline` but never both, one or two `provisioner "shell"` blocks:
|
|
@@ -197,7 +218,7 @@ calls, in order:
|
|
|
197
218
|
with a `configuration_user`, `chmod +x {{ .Path }}; sudo su - <user>
|
|
198
219
|
-c '{{ .Vars }} {{ .Path }}'` (Packer's `{{ .Vars }}`, the
|
|
199
220
|
environment variables, travel inside the command because `su -`
|
|
200
|
-
starts a fresh login environment), else none
|
|
221
|
+
starts a fresh login environment), else none.
|
|
201
222
|
- Every string is rendered as an HCL literal: backslashes and double
|
|
202
223
|
quotes are escaped, and `${` / `%{` become `$${` / `%%{` so shell text
|
|
203
224
|
such as `dpkg-query -f='${Package}'` survives Packer's template parser.
|
|
@@ -212,9 +233,7 @@ modification bundle records the item: its directory
|
|
|
212
233
|
`run.sh` that runs `sh '<script>'` per script file and then
|
|
213
234
|
`sh ./inline.sh`, and a `MANIFEST.yaml` with `operation: bash` and the
|
|
214
235
|
item's `idempotent` value. `csis-mods rerun` on the image therefore
|
|
215
|
-
re-applies the declarative `ensure` form too
|
|
216
|
-
the `ensure` lines were not in the bundle and a rerun replayed only the
|
|
217
|
-
script files and the `script` lines.
|
|
236
|
+
re-applies the declarative `ensure` form too.
|
|
218
237
|
|
|
219
238
|
## Emission
|
|
220
239
|
|
|
@@ -316,8 +335,8 @@ entry in `cfg/executables.yml`
|
|
|
316
335
|
([the fixture's](../../tests/fixtures/config/cfg/executables.yml) is
|
|
317
336
|
`name: bash`, `binary: /usr/local/bin/bash`, no `version`), because
|
|
318
337
|
`validate` checks every builder's `executable` against that list and every
|
|
319
|
-
run checks every declared entry (
|
|
320
|
-
|
|
338
|
+
run checks every declared entry (the binary must exist as an absolute
|
|
339
|
+
path or a name on `PATH`, `BashVersionChecker` must parse a version
|
|
321
340
|
from `<binary> --version`, and when the entry declares `version:` the parsed
|
|
322
341
|
version must satisfy it). That is the whole extent of the plugin's use of a
|
|
323
342
|
local bash: it is looked at, never run. The fixture declares no floor;
|
|
@@ -354,8 +373,8 @@ in `ensure` lines works there too.
|
|
|
354
373
|
**How the plugin finds things.** The builder by the item's `type:` (the
|
|
355
374
|
builder's name); the item class by the builder's `type: bash-remote`; the
|
|
356
375
|
`bash` entry by the builder's `executable:`; script files by path relative
|
|
357
|
-
to the configuration root (the working directory the load
|
|
358
|
-
`--root-dir`, else
|
|
376
|
+
to the configuration root (the working directory the load changes into:
|
|
377
|
+
`--root-dir`, else the directory the command was started from); the
|
|
359
378
|
build VM through the image builder that hosts the emission. No environment
|
|
360
379
|
variable, profile or file of its own.
|
|
361
380
|
|
|
@@ -365,7 +384,7 @@ variable, profile or file of its own.
|
|
|
365
384
|
|
|
366
385
|
Every key the model accepts. An unknown key is refused at load
|
|
367
386
|
(`extra="forbid"` in the shared model config), and `parameters:` is refused
|
|
368
|
-
by name
|
|
387
|
+
by name.
|
|
369
388
|
|
|
370
389
|
| Field | Type | Default | Meaning |
|
|
371
390
|
|---|---|---|---|
|
|
@@ -379,24 +398,24 @@ by name (retired in stage 26).
|
|
|
379
398
|
| `gitignore` | list[str] | `[]` | Accepted by the base model; nothing in this plugin reads it. |
|
|
380
399
|
| `tags` | mapping | `{}` | Accepted, not read. |
|
|
381
400
|
| `execute_command` | str or null | `null` | Emitted verbatim (HCL-escaped) as `execute_command` on every shell block of every item of this builder. Refused together with `configuration_user`. |
|
|
382
|
-
| `configuration_user` | str or null | `null` |
|
|
401
|
+
| `configuration_user` | str or null | `null` | Every shell block of this builder runs its lines as this user through `execute_command = "chmod +x {{ .Path }}; sudo su - <user> -c '{{ .Vars }} {{ .Path }}'"`. Refused together with `execute_command` (`... both say how the script runs; declare one`). |
|
|
383
402
|
| `environment_vars` | list[str] | `[]` | Emitted as `environment_vars = [...]` on every shell block. |
|
|
384
403
|
| `expect_disconnect` | bool | `false` | When true, `expect_disconnect = true` on every shell block. |
|
|
385
|
-
| `extra_arguments` | | |
|
|
404
|
+
| `extra_arguments` | | | Not a field; refused at load as an unknown key. Use `execute_command`. |
|
|
386
405
|
|
|
387
406
|
### The item (an image's `modifications:` entry whose `type:` names this builder)
|
|
388
407
|
|
|
389
408
|
| Field | Type | Default | Meaning |
|
|
390
409
|
|---|---|---|---|
|
|
391
410
|
| `name` | str | required | Unique within the image. Names the comment line, the bundle directory `NN-<name>` and the lineage record. |
|
|
392
|
-
| `type` | str | `default` | The builder's name, an alias, or `default` (or omitted). Rewritten to the builder's name at load
|
|
411
|
+
| `type` | str | `default` | The builder's name, an alias, or `default` (or omitted). Rewritten to the builder's name at load. Omitted or `default` with no `is_default: true` mod builder: refused at load. |
|
|
393
412
|
| `description` | str or null | `null` | Accepted, not read. |
|
|
394
413
|
| `aliases` | list[str] | `[]` | Accepted, not read. |
|
|
395
414
|
| `tags` | mapping | `{}` | Accepted, not read. |
|
|
396
415
|
| `config` | mapping | `{}` | Not read by the plugin. Part of the modification's content hash, so a change re-bakes the image. Not passed to the shell. |
|
|
397
416
|
| `script` | list[str] | `[]` | Inline lines, literal, in order, after the `ensure` lines. Blank and whitespace-only entries are dropped. |
|
|
398
417
|
| `scripts` | list[str] | `[]` | Script files; each is copied beside the Packer root and run first, in order. Blank entries are dropped. An absolute path is not copied and is emitted as is. |
|
|
399
|
-
| `ensure` | mapping | `{}` | Only the four keys below are allowed; any other key is refused at load. Every entry's values are validated at load
|
|
418
|
+
| `ensure` | mapping | `{}` | Only the four keys below are allowed; any other key is refused at load. Every entry's values are validated at load (`validate_ensure`; see "What it tests and verifies"). |
|
|
400
419
|
|
|
401
420
|
The `ensure` keys:
|
|
402
421
|
|
|
@@ -417,11 +436,11 @@ The `ensure` keys:
|
|
|
417
436
|
`idempotent: unknown`, the same as free-form alone. `ensure` lines always
|
|
418
437
|
precede the `script` lines in the inline block.
|
|
419
438
|
- **`ensure` whose lists are all empty** (`ensure: {packages: []}`) is
|
|
420
|
-
refused at load
|
|
421
|
-
|
|
422
|
-
`Bash modification <name> has no script lines or files; nothing emitted
|
|
423
|
-
|
|
424
|
-
|
|
439
|
+
refused at load, because such an item would count as `declared` and
|
|
440
|
+
emit nothing: the builder would log
|
|
441
|
+
`Bash modification <name> has no script lines or files; nothing emitted`,
|
|
442
|
+
the bake would proceed without a provisioner for it, and the bundle
|
|
443
|
+
directory would hold a `run.sh` that runs nothing.
|
|
425
444
|
- **`commands` with `unless`** is guarded; **without `unless`** the command
|
|
426
445
|
runs on every bake and on every `csis-mods rerun`, so the item is
|
|
427
446
|
idempotent only if the command is.
|
|
@@ -431,7 +450,7 @@ The `ensure` keys:
|
|
|
431
450
|
- **`execute_command` set** puts that template on every block of every
|
|
432
451
|
item of the builder; **`configuration_user` set** instead puts
|
|
433
452
|
`chmod +x {{ .Path }}; sudo su - <user> -c '{{ .Vars }} {{ .Path }}'`
|
|
434
|
-
there, so the lines run as that user
|
|
453
|
+
there, so the lines run as that user; **both set** is refused
|
|
435
454
|
at load; **neither**, Packer's default applies and the lines run as the
|
|
436
455
|
bake user. The same for
|
|
437
456
|
`environment_vars` (emitted only when non-empty) and `expect_disconnect`
|
|
@@ -440,11 +459,10 @@ The `ensure` keys:
|
|
|
440
459
|
`type`** reaches whichever mod builder is `is_default: true`; in the
|
|
441
460
|
fixture that is `ansible-default`, so the item is loaded as an ANSIBLE
|
|
442
461
|
item and its `script`/`scripts`/`ensure` keys are refused as unknown.
|
|
443
|
-
With no `is_default: true` mod builder at all, the load is refused
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
for an item that reaches it anyway.
|
|
447
|
-
became an `AttributeError` at generation.
|
|
462
|
+
With no `is_default: true` mod builder at all, the load is refused:
|
|
463
|
+
`No default builder found in mod_builder list.` first, and the
|
|
464
|
+
orchestrator's `` modification '<name>' names no builder (`type`) ... ``
|
|
465
|
+
for an item that reaches it anyway.
|
|
448
466
|
**`type: <alias>`** behaves exactly as the builder's name: the loader
|
|
449
467
|
rewrites it.
|
|
450
468
|
- **AWS versus GCE runtime**: the lines are identical; only the `only`
|
|
@@ -479,7 +497,7 @@ integer mode no higher than `0777`, and not every kind empty); an item
|
|
|
479
497
|
with none of `script`, `scripts`, `ensure`; an item `type:` that names no
|
|
480
498
|
builder; an item with no `type:` when no mod builder is the default; a
|
|
481
499
|
builder declaring both `configuration_user` and `execute_command`, or
|
|
482
|
-
|
|
500
|
+
`extra_arguments`. Each is a
|
|
483
501
|
`ValueError` (or `KeyError` for the type) that stops the load; the CLI
|
|
484
502
|
prints it and exits 1. Nothing loads, so nothing is generated.
|
|
485
503
|
|
|
@@ -532,81 +550,41 @@ failure prints `FAIL <image>/<mod>: rc=...` on stderr and exits 1. A
|
|
|
532
550
|
|
|
533
551
|
## When it fails
|
|
534
552
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
`ansible-default` with no content; the bash builder wrote raw lines and an
|
|
542
|
-
`exit 0` straight into the build file, so any real bash item failed
|
|
543
|
-
`packer validate`; and `scripts:` was not a field, so files were dropped.
|
|
544
|
-
Fixed at gate 8; guarded by
|
|
545
|
-
[test_v2_gate8_modifications.py](../../tests/test_v2_gate8_modifications.py)
|
|
546
|
-
and [tests/test_bash_provisioner.py](tests/test_bash_provisioner.py). If
|
|
547
|
-
a comment line ever appears outside a `provisioner "shell" {` block, or
|
|
548
|
-
`exit 0` appears in a build file, this regressed.
|
|
549
|
-
- **2026-09-10, the first `full-test`'s mod-test leg could not start a
|
|
550
|
-
container** for an EL 10 image: Docker Hub's `rockylinux` library stops at
|
|
551
|
-
9. The stand-in for `rhel` 10 and above is `almalinux:<version>` since
|
|
552
|
-
(`test_image_for` in [mod_tests.py](../base/src/cs_image_system/base/mod_tests.py)).
|
|
553
|
-
Symptom then: `docker run` failed to pull and the bash item's test never
|
|
554
|
-
ran. If `test-mods` reports `unsupported` with
|
|
555
|
-
`no local test image for the root OS family`, the family/version has no
|
|
556
|
-
stand-in; set `local_test_image` on the OS builder.
|
|
557
|
-
- **Found live, undated, in the sibling shell emitter for image tests**
|
|
558
|
-
([v2_provisioners.py](../packer-plugin/src/cs_image_system/packer_plugin/v2_provisioners.py)):
|
|
559
|
-
an `rpm --qf '%{NAME}'` line broke `packer fmt`, because `%{` and `${`
|
|
560
|
-
open Packer template directives. This plugin's `_hcl_string` escapes both
|
|
561
|
-
the same way. If `packer fmt` or `validate` ever reports
|
|
562
|
-
`Invalid template directive` or `Invalid template interpolation` on a
|
|
563
|
-
line you wrote, the escape regressed; the line in `generated/` should
|
|
564
|
-
read `%%{` / `$${`.
|
|
565
|
-
- **2026-09-20, an assertion that could not fail** (image `tests:`, not this
|
|
566
|
-
plugin, but the same shell and the same trap;
|
|
567
|
-
[OPERATIONS.md](../../docs/OPERATIONS.md), "What a green bake proves").
|
|
568
|
-
Under `sh -e`, a command inside an AND-OR list other than the last does
|
|
569
|
-
not abort the script, so a `script:` line written `check || { echo no; }`
|
|
570
|
-
never fails the build however false `check` is. Two bakes went green
|
|
571
|
-
asserting a package that did not exist. If a `script` line must be able
|
|
572
|
-
to stop the bake, end it in something that exits nonzero (`|| exit 1`),
|
|
573
|
-
not in a braced group that returns 0. The plugin's own `commands` form,
|
|
574
|
-
`( unless ) || { run; }`, is deliberate: the group is the LAST element,
|
|
575
|
-
so a failing `run` does abort.
|
|
576
|
-
- **2026-09-23, an item declared with the builder's alias failed at
|
|
577
|
-
generation** (found while writing this README, on a copy of the fixture
|
|
578
|
-
with `type: bash` on `derivative-setup`). Load and `validate` passed; the
|
|
579
|
-
run log then showed
|
|
580
|
-
`AssertionError: No mod builder bash found in context for imgfile-basic-dask`
|
|
581
|
-
and the summary was `ok: false` with no validation errors. Fixed by
|
|
582
|
-
stage 63 item 15 (2026-09-25): the loader rewrites the alias to the
|
|
583
|
-
builder's name, and `tests/test_v2_defects_loading.py` generates that
|
|
584
|
-
same fixture copy.
|
|
585
|
-
|
|
586
|
-
Failures the code raises that have not been seen outside tests:
|
|
553
|
+
Incidents that have happened, with their cause and fix, are recorded on
|
|
554
|
+
[INCIDENTS.md](../../docs/INCIDENTS.md); what follows keeps each
|
|
555
|
+
failure's symptom, its meaning and what to do.
|
|
556
|
+
|
|
557
|
+
Symptoms of the emission and of the bake, with what each means and what
|
|
558
|
+
to do:
|
|
587
559
|
|
|
588
560
|
| What you see | Where | Meaning and what to do |
|
|
589
561
|
|---|---|---|
|
|
562
|
+
| A `# Modifications for ...` comment line outside a `provisioner "shell" {` block, or `exit 0` in a build file | `generated/`; `packer validate` fails | The builder wrote raw lines into the build file instead of provisioner blocks. [tests/test_bash_provisioner.py](tests/test_bash_provisioner.py) and [test_modifications.py](../../tests/test_modifications.py) pin the block form; this is a regression of the plugin, not of the item. |
|
|
563
|
+
| A bash item recorded in lineage as `ansible-default` with no content, or its script files dropped | `meta-state/lineage.yaml` after a bake | The item was resolved to the default mod builder instead of the builder its `type:` names. The same two tests pin the lookup by the item's `type`; check the item's `type:` names this builder. |
|
|
564
|
+
| `test-mods` reports `unsupported` with `no local test image for the root OS family` | `test-mods` | The OS family and version have no stand-in container image (`test_image_for` in [mod_tests.py](../base/src/cs_image_system/base/mod_tests.py)): Docker Hub's `rockylinux` library stops at 9, so `rhel` 10 and above stand in as `almalinux:<version>`, and a family outside that table has none. Set `local_test_image` on the OS builder. |
|
|
565
|
+
| `Invalid template directive` or `Invalid template interpolation` from `packer fmt` or `validate` on a line you wrote | the Packer plugin's run-now commands | `%{` and `${` open Packer template directives; this plugin's `_hcl_string` escapes both as `%%{` / `$${` (the sibling shell emitter for image tests, [v2_provisioners.py](../packer-plugin/src/cs_image_system/packer_plugin/v2_provisioners.py), does the same). If the line in `generated/` does not read so, the escape regressed. |
|
|
566
|
+
| A `script:` line written `check || { echo no; }` never fails the build however false `check` is | the bake, green | Under `sh -e` a command inside an AND-OR list other than the last does not abort the script, so a bake can go green asserting a package that does not exist (the same trap holds for an image's `tests:`; [OPERATIONS.md](../../docs/OPERATIONS.md), "What a green bake proves"). If a `script` line must be able to stop the bake, end it in something that exits nonzero (`\|\| exit 1`), not in a braced group that returns 0. The plugin's own `commands` form, `( unless ) \|\| { run; }`, is deliberate: the group is the LAST element, so a failing `run` does abort. |
|
|
567
|
+
| `AssertionError: No mod builder bash found in context for <image>` in the run log, summary `ok: false` with no validation errors, after load and `validate` passed | generation | An item's `type:` reached generation as the builder's alias; the loader rewrites an alias to the builder's name at load (`tests/test_modifications.py` generates a fixture copy with `type: bash`), so a tree whose loader does not is on an older form ([the stage index](../../docs/CHANGES.md#s63)). Write the builder's name, or update. |
|
|
590
568
|
| `Bash modification '<name>' must supply 'script' (inline lines), 'scripts' (script files) and/or 'ensure' (declarative, idempotent)` | load; CLI exit 1 | The item has none of the three (a `config:`-only item). Add one or remove the item. |
|
|
591
569
|
| `Bash modification '<name>': unknown ensure keys ['users']` | load; CLI exit 1 | Only `packages`, `files`, `services`, `commands` exist. Move anything else to `commands`. |
|
|
592
570
|
| A pydantic error naming a key (`Extra inputs are not permitted`) on the builder or the item | load; CLI exit 1 | A misspelt or foreign key (`playbooks:` on a bash item, `script:` on an ansible item). Check the item's `type:` reaches the builder you meant; `type: default` reaches the default builder, which in the fixture is ansible. |
|
|
593
|
-
| ``<builder>: `parameters` was retired
|
|
571
|
+
| ``<builder>: `parameters` was retired ...`` | load; CLI exit 1 | Delete `parameters:` from the builder. |
|
|
594
572
|
| `KeyError: Could not resolve builder '<type>' referenced in Image(...)` | load; CLI exit 1 | The item's `type:` names no `mod_builders:` entry. |
|
|
595
|
-
| `` mod builder '<name>': `configuration_user` and `execute_command` both say how the script runs; declare one `` | load; CLI exit 1 |
|
|
596
|
-
| A pydantic unknown-key error naming `extra_arguments` on a `bash-remote` builder | load; CLI exit 1 |
|
|
597
|
-
| `No default builder found in mod_builder list.` | load; CLI exit 1 | No mod builder says `is_default: true`. Mark one, or give every item a `type:`. The orchestrator's own form, for an item that reaches it, is `` modification '<name>' names no builder (`type`) and no mod builder is `is_default: true`; name one with `type:` or mark a default
|
|
573
|
+
| `` mod builder '<name>': `configuration_user` and `execute_command` both say how the script runs; declare one `` | load; CLI exit 1 | `configuration_user` writes its own `execute_command`; keep one. To run as another user with a custom command, put the `sudo su - <user>` in your `execute_command`. |
|
|
574
|
+
| A pydantic unknown-key error naming `extra_arguments` on a `bash-remote` builder | load; CLI exit 1 | The field does not exist. Delete it; change the invocation with `execute_command`. |
|
|
575
|
+
| `No default builder found in mod_builder list.` | load; CLI exit 1 | No mod builder says `is_default: true`. Mark one, or give every item a `type:`. The orchestrator's own form, for an item that reaches it, is `` modification '<name>' names no builder (`type`) and no mod builder is `is_default: true`; name one with `type:` or mark a default ``. |
|
|
598
576
|
| `Executable bash specified for provider bash-remote not found in executables list.` | `validate`; exit 1 | Add a `bash` entry to `cfg/executables.yml` or change the builder's `executable:`. |
|
|
599
577
|
| `bash: binary '/usr/local/bin/bash' not found (declared in cfg/executables.yml; an absolute path, or a name on PATH)` | `validate` and every run; exit 1 | The pinned path is wrong on this machine. Symlink the binary there or fix `binary:`. |
|
|
600
578
|
| ``bash: BashVersionChecker could not parse a version from `/usr/local/bin/bash --version` `` | `validate` and every run | The first line of `--version` has no `version N.N`. Something other than bash is at that path. |
|
|
601
579
|
| `bash 5.3.15 does not meet its requirement <spec> (cfg/executables.yml)` | `validate` and every run | The floor moved or the machine is behind. |
|
|
602
580
|
| `FileNotFoundError: Bash modification script mod_image.sh does not exist or is not a file` | generation; run `ok: false`, traceback in the log | The path is resolved from the configuration root, not from the image file's directory or the shell's. Fix the path or `--root-dir`. |
|
|
603
581
|
| `Bash modification '<name>': ensure declares [...] but every one is empty, so it would emit nothing` | load; CLI exit 1 | Every `ensure` list is empty. Fill one or drop `ensure`. |
|
|
604
|
-
| `Bash modification '<name>': ensure.files[N]: `path` is required` (or `ensure.commands[N]: `run` is required`) | load; CLI exit 1 | A `files` entry without `path`, or a `commands` entry without `run`.
|
|
605
|
-
| `Bash modification '<name>': ensure.packages must be a list of package names, not str 'git'` | load; CLI exit 1 | `packages:` (or `services:`) given as a string
|
|
582
|
+
| `Bash modification '<name>': ensure.files[N]: `path` is required` (or `ensure.commands[N]: `run` is required`) | load; CLI exit 1 | A `files` entry without `path`, or a `commands` entry without `run`. |
|
|
583
|
+
| `Bash modification '<name>': ensure.packages must be a list of package names, not str 'git'` | load; CLI exit 1 | `packages:` (or `services:`) given as a string, which shell would iterate letter by letter. Write a list (`packages: [git]`). |
|
|
606
584
|
| `Bash modification '<name>': ensure.files[N].mode 644 is not a mode YAML could have read from octal` | load; CLI exit 1 | An unquoted `644`, which YAML reads as a decimal number. Write `mode: "0644"`. |
|
|
607
585
|
| `Bash modification '<name>': ensure.files[N]: unknown keys [...]` | load; CLI exit 1 | Only `path`, `content`, `mode` on a file and `run`, `unless` on a command. |
|
|
608
586
|
| `csis ensure: no dnf, yum or apt-get to install <pkg>` | apply / `test-mods` | The image has none of the three package managers. Use a `script` line with the one it has. |
|
|
609
|
-
| `dnf` (or `yum`, `apt-get`) error on a package step | apply / `test-mods` | The first package manager found could not install the package, and its message is the real one (no match, no repository, no network from the build subnet)
|
|
587
|
+
| `dnf` (or `yum`, `apt-get`) error on a package step | apply / `test-mods` | The first package manager found could not install the package, and its message is the real one (no match, no repository, no network from the build subnet); the others are never tried, so no misleading `apt-get: command not found` follows it on an EL host. |
|
|
610
588
|
| `cmp: command not found` or `base64: command not found` | apply | The image lacks diffutils / coreutils. Add the package to an earlier item's `ensure.packages` or a `script` line. |
|
|
611
589
|
| `sudo: a terminal is required to read the password` | apply | The bake ssh user has no passwordless `sudo`. This is an OS builder / runtime matter, not the item's. |
|
|
612
590
|
| `Failed to enable unit: Unit <svc>.service does not exist` | apply | `services:` names a unit the image has not installed yet; install it earlier in the same item's `packages` (packages run before services). |
|
|
@@ -638,20 +616,20 @@ Failures the code raises that have not been seen outside tests:
|
|
|
638
616
|
[abstract_version_checker.py](../base/src/cs_image_system/base/basic/abstract_version_checker.py).
|
|
639
617
|
- The image builder that calls it and hosts its output:
|
|
640
618
|
[packer-plugin](../packer-plugin)
|
|
641
|
-
([
|
|
619
|
+
([packer_image_builder.py](../packer-plugin/src/cs_image_system/packer_plugin/packer_image_builder.py),
|
|
642
620
|
[v2_local_mods.py](../packer-plugin/src/cs_image_system/packer_plugin/v2_local_mods.py)).
|
|
643
621
|
- Lineage records for modifications:
|
|
644
622
|
[lineage.py](../base/src/cs_image_system/base/lineage.py) (`mod_records`);
|
|
645
623
|
the release gate in [release.py](../base/src/cs_image_system/base/release.py).
|
|
646
624
|
- Tests: [tests/test_bash_provisioner.py](tests/test_bash_provisioner.py)
|
|
647
625
|
(two blocks for files plus inline, builder options, script copying);
|
|
648
|
-
[
|
|
626
|
+
[test_modifications.py](../../tests/test_modifications.py)
|
|
649
627
|
(both kinds emitted, unknown type refused, the load rule);
|
|
650
|
-
[
|
|
628
|
+
[test_modifications.py](../../tests/test_modifications.py)
|
|
651
629
|
(`ensure` guards, idempotence recorded, the bundle);
|
|
652
|
-
[
|
|
630
|
+
[test_mod_tests.py](../../tests/test_mod_tests.py)
|
|
653
631
|
(the container tests on the fixture's bash item);
|
|
654
|
-
[
|
|
632
|
+
[test_version_checks.py](../../tests/test_version_checks.py)
|
|
655
633
|
(`BashVersionChecker` found by name, versions judged).
|
|
656
634
|
- No terraform modules and no hashicorp-utils usage: the plugin writes
|
|
657
635
|
Packer provisioner lines directly.
|
|
@@ -39,18 +39,15 @@ How a YAML entry selects these classes:
|
|
|
39
39
|
looks the builder up by name or alias, reads the builder's own `type`
|
|
40
40
|
(`bash-remote`) and loads the item into the class registered under that
|
|
41
41
|
service key for `MOD_BUILDER_ITEM_MODEL`: `BashModItemModel`. The
|
|
42
|
-
registry alone decides
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
empty `helpers.py`.
|
|
46
|
-
- **The item's `type` is rewritten to the builder's `name` at load**
|
|
47
|
-
(stage 63 item 15). The image builder later fetches the builder with
|
|
42
|
+
registry alone decides.
|
|
43
|
+
- **The item's `type` is rewritten to the builder's `name` at load.**
|
|
44
|
+
The image builder later fetches the builder with
|
|
48
45
|
`ctx.mod_builders.get(mod.get_type())`, a map keyed by builder NAME
|
|
49
|
-
only, so an alias must not survive the load
|
|
50
|
-
|
|
51
|
-
`type: bash-remote`. Before 2026-09-25 the alias was kept, and
|
|
52
|
-
generation stopped with
|
|
46
|
+
only, so an alias must not survive the load: a kept alias would stop
|
|
47
|
+
generation with
|
|
53
48
|
`AssertionError: No mod builder bash found in context for <image>`.
|
|
49
|
+
An item declared `type: bash` (the fixture builder's alias) is loaded
|
|
50
|
+
as `type: bash-remote`.
|
|
54
51
|
- An `executables:` entry named `bash` is version-checked by
|
|
55
52
|
`BashVersionChecker` (regex `.*version\s+([\d\.]+)` over
|
|
56
53
|
`bash --version`).
|
|
@@ -64,21 +61,31 @@ Defined in [bash_models.py](src/cs_image_system/bash_mod_plugin/bash_models.py).
|
|
|
64
61
|
[`ModBuilderModel`](../base/src/cs_image_system/base/models/mod_builder.py),
|
|
65
62
|
which adds nothing to
|
|
66
63
|
[`BuilderModel`](../base/src/cs_image_system/base/models/builder_model.py)
|
|
67
|
-
(`name`, `type`, `description`, `aliases`,
|
|
68
|
-
`config`, `gitignore`, `tags
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
64
|
+
(the common builder fields: `name`, `type`, `description`, `aliases`,
|
|
65
|
+
`executable`, `is_default`, `config`, `gitignore`, `tags`;
|
|
66
|
+
[CONFIGURATION section 4.1](../../docs/CONFIGURATION.md#41-fields-common-to-every-builder)
|
|
67
|
+
holds that table). `BashModBuilderModel` is the registered subclass; it
|
|
68
|
+
adds no fields.
|
|
69
|
+
|
|
70
|
+
The keys this plugin adds are `execute_command`, `configuration_user`,
|
|
71
|
+
`environment_vars` and `expect_disconnect`: each is a Packer
|
|
72
|
+
shell-provisioner argument, emitted on every shell provisioner of every
|
|
73
|
+
item of the builder.
|
|
74
|
+
[CONFIGURATION section 7.2](../../docs/CONFIGURATION.md#72-bash-remote)
|
|
75
|
+
holds the field table; [Configuration reference](#configuration-reference)
|
|
76
|
+
below gives this plugin's reading of each key. Rules only this plugin
|
|
77
|
+
enforces:
|
|
78
|
+
|
|
79
|
+
- `execute_command` (Packer's command template, for example
|
|
80
|
+
`sudo -E bash '{{.Path}}'`) is the way to change how the script is
|
|
81
|
+
invoked. `configuration_user` writes its own: every shell provisioner
|
|
82
|
+
of the builder gets `execute_command = "chmod +x {{ .Path }}; sudo su -
|
|
83
|
+
<user> -c '{{ .Vars }} {{ .Path }}'"`, so each item's lines run as that
|
|
84
|
+
user and the bake user only launches them. Because both say how the
|
|
85
|
+
script runs, declaring them together is refused at load.
|
|
86
|
+
- `extra_arguments` is not a field: a builder that declares it fails to
|
|
87
|
+
load with the unknown-key refusal. Use `execute_command` to change the
|
|
88
|
+
invocation.
|
|
82
89
|
|
|
83
90
|
Base fields this plugin constrains: `executable` should name the `bash`
|
|
84
91
|
executable (the fixture does), though the emission does not invoke it;
|
|
@@ -89,13 +96,28 @@ base-model rule). `config:` on the builder is carried but never read.
|
|
|
89
96
|
|
|
90
97
|
Same file. Extends
|
|
91
98
|
[`ModItemModel`](../base/src/cs_image_system/base/models/moditem_type.py)
|
|
92
|
-
(`SubRootItem` -> `RootItem` -> `NameTyped`).
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
(`SubRootItem` -> `RootItem` -> `NameTyped`). The item's own keys are
|
|
100
|
+
`script`, `scripts` and `ensure`; the inherited ones (`name`, `type`,
|
|
101
|
+
`description`, `aliases`, `tags`, `config`) are the modification item's,
|
|
102
|
+
tabled in
|
|
103
|
+
[CONFIGURATION section 7.3](../../docs/CONFIGURATION.md#73-the-modification-item-inside-an-images-modifications),
|
|
104
|
+
and [Configuration reference](#configuration-reference) below reads every
|
|
105
|
+
key as this plugin does.
|
|
106
|
+
|
|
107
|
+
- `script` is the list of inline command lines, run in order in one
|
|
108
|
+
`provisioner "shell"` (`inline = [...]`). Literal shell; no templating.
|
|
109
|
+
Blank entries are dropped.
|
|
110
|
+
- `scripts` is the list of script files, relative to the configuration
|
|
111
|
+
root (the process's working directory, which the configuration load
|
|
112
|
+
sets to the root), copied beside the Packer root and run as
|
|
113
|
+
`scripts = [...]`. Blank entries are dropped.
|
|
114
|
+
- `ensure` is the declarative form. Allowed keys: `packages` (list of
|
|
115
|
+
names), `files` (list of `{path, content, mode}`; `mode` defaults to
|
|
116
|
+
`0644`), `services` (list of unit names, enabled and started),
|
|
117
|
+
`commands` (list of `{run, unless}`; `run` executes only when `unless`
|
|
118
|
+
fails). Every entry is validated at load by `validate_ensure`: a wrong
|
|
119
|
+
shape, an unknown key, a bad mode or an all-empty mapping raises
|
|
120
|
+
`ValueError` naming the item and the entry.
|
|
99
121
|
|
|
100
122
|
Validation at load: at least one of `script`, `scripts`, `ensure` must be
|
|
101
123
|
non-empty, otherwise `ValueError` ("must supply 'script' (inline lines),
|
|
@@ -118,15 +140,13 @@ Derived behaviour:
|
|
|
118
140
|
- `remap_self_with_copied_assets(copied)`: replaces each `scripts` entry
|
|
119
141
|
with its copied path when present in the mapping.
|
|
120
142
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
| `config` | `RootItem` | `{}` | Not read by this plugin. It takes part in the lineage content hash of the modification and nothing else. |
|
|
129
|
-
| `_model_id` | `ModItemModel` | set at load | The owning image's global id; `identified_model` resolves to the image. |
|
|
143
|
+
Of the inherited fields, `name` names the provisioner comment and the
|
|
144
|
+
local bundle directory (`NN-<name>`); `type` is the foreign key to a
|
|
145
|
+
`mod_builders:` entry (see above); `description`, `aliases` and `tags`
|
|
146
|
+
are carried and not emitted; `config` is not read by this plugin and
|
|
147
|
+
takes part in the lineage content hash of the modification and nothing
|
|
148
|
+
else; `_model_id`, set at load, is the owning image's global id, and
|
|
149
|
+
`identified_model` resolves to the image.
|
|
130
150
|
|
|
131
151
|
### A modification item with only `config:`
|
|
132
152
|
|
|
@@ -155,10 +175,11 @@ calls, in order:
|
|
|
155
175
|
`scripts` to `<block_dir>/<script path>` and returns
|
|
156
176
|
`{script: script path}`. The source is resolved against the process's
|
|
157
177
|
current working directory; the configuration load has already changed
|
|
158
|
-
directory to the configuration root (`--root-dir`, else
|
|
159
|
-
`working_directory` in `cfg/_config.yml
|
|
160
|
-
path is root-relative. A missing file raises
|
|
161
|
-
absolute path is not copied and is left in
|
|
178
|
+
directory to the configuration root (`--root-dir`, else the directory
|
|
179
|
+
the command was started from; `working_directory` in `cfg/_config.yml`
|
|
180
|
+
is not read), so the path is root-relative. A missing file raises
|
|
181
|
+
`FileNotFoundError`; an absolute path is not copied and is left in
|
|
182
|
+
place.
|
|
162
183
|
2. `mod.remap_self_with_copied_assets(mapping)` on the item. Because the
|
|
163
184
|
mapping's value is the same relative path the copy was placed at, the
|
|
164
185
|
item's `scripts` entries keep their text; the emitted `scripts = [...]`
|
|
@@ -167,8 +188,8 @@ calls, in order:
|
|
|
167
188
|
4. `generate_items_during_modification(image, mod, image_builder, phase, build_path)`:
|
|
168
189
|
- `inline` = `ensure_lines()` followed by the `script` lines; `scripts`
|
|
169
190
|
= the script files. When both lists are empty it logs a warning and
|
|
170
|
-
emits nothing;
|
|
171
|
-
|
|
191
|
+
emits nothing; an all-empty `ensure` is refused at load, so an item
|
|
192
|
+
cannot reach this.
|
|
172
193
|
- Appends `# Modifications for <item name> of type <item type> (shell)`.
|
|
173
194
|
- Then, because Packer's shell provisioner takes either `scripts` or
|
|
174
195
|
`inline` but never both, one or two `provisioner "shell"` blocks:
|
|
@@ -181,7 +202,7 @@ calls, in order:
|
|
|
181
202
|
with a `configuration_user`, `chmod +x {{ .Path }}; sudo su - <user>
|
|
182
203
|
-c '{{ .Vars }} {{ .Path }}'` (Packer's `{{ .Vars }}`, the
|
|
183
204
|
environment variables, travel inside the command because `su -`
|
|
184
|
-
starts a fresh login environment), else none
|
|
205
|
+
starts a fresh login environment), else none.
|
|
185
206
|
- Every string is rendered as an HCL literal: backslashes and double
|
|
186
207
|
quotes are escaped, and `${` / `%{` become `$${` / `%%{` so shell text
|
|
187
208
|
such as `dpkg-query -f='${Package}'` survives Packer's template parser.
|
|
@@ -196,9 +217,7 @@ modification bundle records the item: its directory
|
|
|
196
217
|
`run.sh` that runs `sh '<script>'` per script file and then
|
|
197
218
|
`sh ./inline.sh`, and a `MANIFEST.yaml` with `operation: bash` and the
|
|
198
219
|
item's `idempotent` value. `csis-mods rerun` on the image therefore
|
|
199
|
-
re-applies the declarative `ensure` form too
|
|
200
|
-
the `ensure` lines were not in the bundle and a rerun replayed only the
|
|
201
|
-
script files and the `script` lines.
|
|
220
|
+
re-applies the declarative `ensure` form too.
|
|
202
221
|
|
|
203
222
|
## Emission
|
|
204
223
|
|
|
@@ -300,8 +319,8 @@ entry in `cfg/executables.yml`
|
|
|
300
319
|
([the fixture's](../../tests/fixtures/config/cfg/executables.yml) is
|
|
301
320
|
`name: bash`, `binary: /usr/local/bin/bash`, no `version`), because
|
|
302
321
|
`validate` checks every builder's `executable` against that list and every
|
|
303
|
-
run checks every declared entry (
|
|
304
|
-
|
|
322
|
+
run checks every declared entry (the binary must exist as an absolute
|
|
323
|
+
path or a name on `PATH`, `BashVersionChecker` must parse a version
|
|
305
324
|
from `<binary> --version`, and when the entry declares `version:` the parsed
|
|
306
325
|
version must satisfy it). That is the whole extent of the plugin's use of a
|
|
307
326
|
local bash: it is looked at, never run. The fixture declares no floor;
|
|
@@ -338,8 +357,8 @@ in `ensure` lines works there too.
|
|
|
338
357
|
**How the plugin finds things.** The builder by the item's `type:` (the
|
|
339
358
|
builder's name); the item class by the builder's `type: bash-remote`; the
|
|
340
359
|
`bash` entry by the builder's `executable:`; script files by path relative
|
|
341
|
-
to the configuration root (the working directory the load
|
|
342
|
-
`--root-dir`, else
|
|
360
|
+
to the configuration root (the working directory the load changes into:
|
|
361
|
+
`--root-dir`, else the directory the command was started from); the
|
|
343
362
|
build VM through the image builder that hosts the emission. No environment
|
|
344
363
|
variable, profile or file of its own.
|
|
345
364
|
|
|
@@ -349,7 +368,7 @@ variable, profile or file of its own.
|
|
|
349
368
|
|
|
350
369
|
Every key the model accepts. An unknown key is refused at load
|
|
351
370
|
(`extra="forbid"` in the shared model config), and `parameters:` is refused
|
|
352
|
-
by name
|
|
371
|
+
by name.
|
|
353
372
|
|
|
354
373
|
| Field | Type | Default | Meaning |
|
|
355
374
|
|---|---|---|---|
|
|
@@ -363,24 +382,24 @@ by name (retired in stage 26).
|
|
|
363
382
|
| `gitignore` | list[str] | `[]` | Accepted by the base model; nothing in this plugin reads it. |
|
|
364
383
|
| `tags` | mapping | `{}` | Accepted, not read. |
|
|
365
384
|
| `execute_command` | str or null | `null` | Emitted verbatim (HCL-escaped) as `execute_command` on every shell block of every item of this builder. Refused together with `configuration_user`. |
|
|
366
|
-
| `configuration_user` | str or null | `null` |
|
|
385
|
+
| `configuration_user` | str or null | `null` | Every shell block of this builder runs its lines as this user through `execute_command = "chmod +x {{ .Path }}; sudo su - <user> -c '{{ .Vars }} {{ .Path }}'"`. Refused together with `execute_command` (`... both say how the script runs; declare one`). |
|
|
367
386
|
| `environment_vars` | list[str] | `[]` | Emitted as `environment_vars = [...]` on every shell block. |
|
|
368
387
|
| `expect_disconnect` | bool | `false` | When true, `expect_disconnect = true` on every shell block. |
|
|
369
|
-
| `extra_arguments` | | |
|
|
388
|
+
| `extra_arguments` | | | Not a field; refused at load as an unknown key. Use `execute_command`. |
|
|
370
389
|
|
|
371
390
|
### The item (an image's `modifications:` entry whose `type:` names this builder)
|
|
372
391
|
|
|
373
392
|
| Field | Type | Default | Meaning |
|
|
374
393
|
|---|---|---|---|
|
|
375
394
|
| `name` | str | required | Unique within the image. Names the comment line, the bundle directory `NN-<name>` and the lineage record. |
|
|
376
|
-
| `type` | str | `default` | The builder's name, an alias, or `default` (or omitted). Rewritten to the builder's name at load
|
|
395
|
+
| `type` | str | `default` | The builder's name, an alias, or `default` (or omitted). Rewritten to the builder's name at load. Omitted or `default` with no `is_default: true` mod builder: refused at load. |
|
|
377
396
|
| `description` | str or null | `null` | Accepted, not read. |
|
|
378
397
|
| `aliases` | list[str] | `[]` | Accepted, not read. |
|
|
379
398
|
| `tags` | mapping | `{}` | Accepted, not read. |
|
|
380
399
|
| `config` | mapping | `{}` | Not read by the plugin. Part of the modification's content hash, so a change re-bakes the image. Not passed to the shell. |
|
|
381
400
|
| `script` | list[str] | `[]` | Inline lines, literal, in order, after the `ensure` lines. Blank and whitespace-only entries are dropped. |
|
|
382
401
|
| `scripts` | list[str] | `[]` | Script files; each is copied beside the Packer root and run first, in order. Blank entries are dropped. An absolute path is not copied and is emitted as is. |
|
|
383
|
-
| `ensure` | mapping | `{}` | Only the four keys below are allowed; any other key is refused at load. Every entry's values are validated at load
|
|
402
|
+
| `ensure` | mapping | `{}` | Only the four keys below are allowed; any other key is refused at load. Every entry's values are validated at load (`validate_ensure`; see "What it tests and verifies"). |
|
|
384
403
|
|
|
385
404
|
The `ensure` keys:
|
|
386
405
|
|
|
@@ -401,11 +420,11 @@ The `ensure` keys:
|
|
|
401
420
|
`idempotent: unknown`, the same as free-form alone. `ensure` lines always
|
|
402
421
|
precede the `script` lines in the inline block.
|
|
403
422
|
- **`ensure` whose lists are all empty** (`ensure: {packages: []}`) is
|
|
404
|
-
refused at load
|
|
405
|
-
|
|
406
|
-
`Bash modification <name> has no script lines or files; nothing emitted
|
|
407
|
-
|
|
408
|
-
|
|
423
|
+
refused at load, because such an item would count as `declared` and
|
|
424
|
+
emit nothing: the builder would log
|
|
425
|
+
`Bash modification <name> has no script lines or files; nothing emitted`,
|
|
426
|
+
the bake would proceed without a provisioner for it, and the bundle
|
|
427
|
+
directory would hold a `run.sh` that runs nothing.
|
|
409
428
|
- **`commands` with `unless`** is guarded; **without `unless`** the command
|
|
410
429
|
runs on every bake and on every `csis-mods rerun`, so the item is
|
|
411
430
|
idempotent only if the command is.
|
|
@@ -415,7 +434,7 @@ The `ensure` keys:
|
|
|
415
434
|
- **`execute_command` set** puts that template on every block of every
|
|
416
435
|
item of the builder; **`configuration_user` set** instead puts
|
|
417
436
|
`chmod +x {{ .Path }}; sudo su - <user> -c '{{ .Vars }} {{ .Path }}'`
|
|
418
|
-
there, so the lines run as that user
|
|
437
|
+
there, so the lines run as that user; **both set** is refused
|
|
419
438
|
at load; **neither**, Packer's default applies and the lines run as the
|
|
420
439
|
bake user. The same for
|
|
421
440
|
`environment_vars` (emitted only when non-empty) and `expect_disconnect`
|
|
@@ -424,11 +443,10 @@ The `ensure` keys:
|
|
|
424
443
|
`type`** reaches whichever mod builder is `is_default: true`; in the
|
|
425
444
|
fixture that is `ansible-default`, so the item is loaded as an ANSIBLE
|
|
426
445
|
item and its `script`/`scripts`/`ensure` keys are refused as unknown.
|
|
427
|
-
With no `is_default: true` mod builder at all, the load is refused
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
for an item that reaches it anyway.
|
|
431
|
-
became an `AttributeError` at generation.
|
|
446
|
+
With no `is_default: true` mod builder at all, the load is refused:
|
|
447
|
+
`No default builder found in mod_builder list.` first, and the
|
|
448
|
+
orchestrator's `` modification '<name>' names no builder (`type`) ... ``
|
|
449
|
+
for an item that reaches it anyway.
|
|
432
450
|
**`type: <alias>`** behaves exactly as the builder's name: the loader
|
|
433
451
|
rewrites it.
|
|
434
452
|
- **AWS versus GCE runtime**: the lines are identical; only the `only`
|
|
@@ -463,7 +481,7 @@ integer mode no higher than `0777`, and not every kind empty); an item
|
|
|
463
481
|
with none of `script`, `scripts`, `ensure`; an item `type:` that names no
|
|
464
482
|
builder; an item with no `type:` when no mod builder is the default; a
|
|
465
483
|
builder declaring both `configuration_user` and `execute_command`, or
|
|
466
|
-
|
|
484
|
+
`extra_arguments`. Each is a
|
|
467
485
|
`ValueError` (or `KeyError` for the type) that stops the load; the CLI
|
|
468
486
|
prints it and exits 1. Nothing loads, so nothing is generated.
|
|
469
487
|
|
|
@@ -516,81 +534,41 @@ failure prints `FAIL <image>/<mod>: rc=...` on stderr and exits 1. A
|
|
|
516
534
|
|
|
517
535
|
## When it fails
|
|
518
536
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
`ansible-default` with no content; the bash builder wrote raw lines and an
|
|
526
|
-
`exit 0` straight into the build file, so any real bash item failed
|
|
527
|
-
`packer validate`; and `scripts:` was not a field, so files were dropped.
|
|
528
|
-
Fixed at gate 8; guarded by
|
|
529
|
-
[test_v2_gate8_modifications.py](../../tests/test_v2_gate8_modifications.py)
|
|
530
|
-
and [tests/test_bash_provisioner.py](tests/test_bash_provisioner.py). If
|
|
531
|
-
a comment line ever appears outside a `provisioner "shell" {` block, or
|
|
532
|
-
`exit 0` appears in a build file, this regressed.
|
|
533
|
-
- **2026-09-10, the first `full-test`'s mod-test leg could not start a
|
|
534
|
-
container** for an EL 10 image: Docker Hub's `rockylinux` library stops at
|
|
535
|
-
9. The stand-in for `rhel` 10 and above is `almalinux:<version>` since
|
|
536
|
-
(`test_image_for` in [mod_tests.py](../base/src/cs_image_system/base/mod_tests.py)).
|
|
537
|
-
Symptom then: `docker run` failed to pull and the bash item's test never
|
|
538
|
-
ran. If `test-mods` reports `unsupported` with
|
|
539
|
-
`no local test image for the root OS family`, the family/version has no
|
|
540
|
-
stand-in; set `local_test_image` on the OS builder.
|
|
541
|
-
- **Found live, undated, in the sibling shell emitter for image tests**
|
|
542
|
-
([v2_provisioners.py](../packer-plugin/src/cs_image_system/packer_plugin/v2_provisioners.py)):
|
|
543
|
-
an `rpm --qf '%{NAME}'` line broke `packer fmt`, because `%{` and `${`
|
|
544
|
-
open Packer template directives. This plugin's `_hcl_string` escapes both
|
|
545
|
-
the same way. If `packer fmt` or `validate` ever reports
|
|
546
|
-
`Invalid template directive` or `Invalid template interpolation` on a
|
|
547
|
-
line you wrote, the escape regressed; the line in `generated/` should
|
|
548
|
-
read `%%{` / `$${`.
|
|
549
|
-
- **2026-09-20, an assertion that could not fail** (image `tests:`, not this
|
|
550
|
-
plugin, but the same shell and the same trap;
|
|
551
|
-
[OPERATIONS.md](../../docs/OPERATIONS.md), "What a green bake proves").
|
|
552
|
-
Under `sh -e`, a command inside an AND-OR list other than the last does
|
|
553
|
-
not abort the script, so a `script:` line written `check || { echo no; }`
|
|
554
|
-
never fails the build however false `check` is. Two bakes went green
|
|
555
|
-
asserting a package that did not exist. If a `script` line must be able
|
|
556
|
-
to stop the bake, end it in something that exits nonzero (`|| exit 1`),
|
|
557
|
-
not in a braced group that returns 0. The plugin's own `commands` form,
|
|
558
|
-
`( unless ) || { run; }`, is deliberate: the group is the LAST element,
|
|
559
|
-
so a failing `run` does abort.
|
|
560
|
-
- **2026-09-23, an item declared with the builder's alias failed at
|
|
561
|
-
generation** (found while writing this README, on a copy of the fixture
|
|
562
|
-
with `type: bash` on `derivative-setup`). Load and `validate` passed; the
|
|
563
|
-
run log then showed
|
|
564
|
-
`AssertionError: No mod builder bash found in context for imgfile-basic-dask`
|
|
565
|
-
and the summary was `ok: false` with no validation errors. Fixed by
|
|
566
|
-
stage 63 item 15 (2026-09-25): the loader rewrites the alias to the
|
|
567
|
-
builder's name, and `tests/test_v2_defects_loading.py` generates that
|
|
568
|
-
same fixture copy.
|
|
569
|
-
|
|
570
|
-
Failures the code raises that have not been seen outside tests:
|
|
537
|
+
Incidents that have happened, with their cause and fix, are recorded on
|
|
538
|
+
[INCIDENTS.md](../../docs/INCIDENTS.md); what follows keeps each
|
|
539
|
+
failure's symptom, its meaning and what to do.
|
|
540
|
+
|
|
541
|
+
Symptoms of the emission and of the bake, with what each means and what
|
|
542
|
+
to do:
|
|
571
543
|
|
|
572
544
|
| What you see | Where | Meaning and what to do |
|
|
573
545
|
|---|---|---|
|
|
546
|
+
| A `# Modifications for ...` comment line outside a `provisioner "shell" {` block, or `exit 0` in a build file | `generated/`; `packer validate` fails | The builder wrote raw lines into the build file instead of provisioner blocks. [tests/test_bash_provisioner.py](tests/test_bash_provisioner.py) and [test_modifications.py](../../tests/test_modifications.py) pin the block form; this is a regression of the plugin, not of the item. |
|
|
547
|
+
| A bash item recorded in lineage as `ansible-default` with no content, or its script files dropped | `meta-state/lineage.yaml` after a bake | The item was resolved to the default mod builder instead of the builder its `type:` names. The same two tests pin the lookup by the item's `type`; check the item's `type:` names this builder. |
|
|
548
|
+
| `test-mods` reports `unsupported` with `no local test image for the root OS family` | `test-mods` | The OS family and version have no stand-in container image (`test_image_for` in [mod_tests.py](../base/src/cs_image_system/base/mod_tests.py)): Docker Hub's `rockylinux` library stops at 9, so `rhel` 10 and above stand in as `almalinux:<version>`, and a family outside that table has none. Set `local_test_image` on the OS builder. |
|
|
549
|
+
| `Invalid template directive` or `Invalid template interpolation` from `packer fmt` or `validate` on a line you wrote | the Packer plugin's run-now commands | `%{` and `${` open Packer template directives; this plugin's `_hcl_string` escapes both as `%%{` / `$${` (the sibling shell emitter for image tests, [v2_provisioners.py](../packer-plugin/src/cs_image_system/packer_plugin/v2_provisioners.py), does the same). If the line in `generated/` does not read so, the escape regressed. |
|
|
550
|
+
| A `script:` line written `check || { echo no; }` never fails the build however false `check` is | the bake, green | Under `sh -e` a command inside an AND-OR list other than the last does not abort the script, so a bake can go green asserting a package that does not exist (the same trap holds for an image's `tests:`; [OPERATIONS.md](../../docs/OPERATIONS.md), "What a green bake proves"). If a `script` line must be able to stop the bake, end it in something that exits nonzero (`\|\| exit 1`), not in a braced group that returns 0. The plugin's own `commands` form, `( unless ) \|\| { run; }`, is deliberate: the group is the LAST element, so a failing `run` does abort. |
|
|
551
|
+
| `AssertionError: No mod builder bash found in context for <image>` in the run log, summary `ok: false` with no validation errors, after load and `validate` passed | generation | An item's `type:` reached generation as the builder's alias; the loader rewrites an alias to the builder's name at load (`tests/test_modifications.py` generates a fixture copy with `type: bash`), so a tree whose loader does not is on an older form ([the stage index](../../docs/CHANGES.md#s63)). Write the builder's name, or update. |
|
|
574
552
|
| `Bash modification '<name>' must supply 'script' (inline lines), 'scripts' (script files) and/or 'ensure' (declarative, idempotent)` | load; CLI exit 1 | The item has none of the three (a `config:`-only item). Add one or remove the item. |
|
|
575
553
|
| `Bash modification '<name>': unknown ensure keys ['users']` | load; CLI exit 1 | Only `packages`, `files`, `services`, `commands` exist. Move anything else to `commands`. |
|
|
576
554
|
| A pydantic error naming a key (`Extra inputs are not permitted`) on the builder or the item | load; CLI exit 1 | A misspelt or foreign key (`playbooks:` on a bash item, `script:` on an ansible item). Check the item's `type:` reaches the builder you meant; `type: default` reaches the default builder, which in the fixture is ansible. |
|
|
577
|
-
| ``<builder>: `parameters` was retired
|
|
555
|
+
| ``<builder>: `parameters` was retired ...`` | load; CLI exit 1 | Delete `parameters:` from the builder. |
|
|
578
556
|
| `KeyError: Could not resolve builder '<type>' referenced in Image(...)` | load; CLI exit 1 | The item's `type:` names no `mod_builders:` entry. |
|
|
579
|
-
| `` mod builder '<name>': `configuration_user` and `execute_command` both say how the script runs; declare one `` | load; CLI exit 1 |
|
|
580
|
-
| A pydantic unknown-key error naming `extra_arguments` on a `bash-remote` builder | load; CLI exit 1 |
|
|
581
|
-
| `No default builder found in mod_builder list.` | load; CLI exit 1 | No mod builder says `is_default: true`. Mark one, or give every item a `type:`. The orchestrator's own form, for an item that reaches it, is `` modification '<name>' names no builder (`type`) and no mod builder is `is_default: true`; name one with `type:` or mark a default
|
|
557
|
+
| `` mod builder '<name>': `configuration_user` and `execute_command` both say how the script runs; declare one `` | load; CLI exit 1 | `configuration_user` writes its own `execute_command`; keep one. To run as another user with a custom command, put the `sudo su - <user>` in your `execute_command`. |
|
|
558
|
+
| A pydantic unknown-key error naming `extra_arguments` on a `bash-remote` builder | load; CLI exit 1 | The field does not exist. Delete it; change the invocation with `execute_command`. |
|
|
559
|
+
| `No default builder found in mod_builder list.` | load; CLI exit 1 | No mod builder says `is_default: true`. Mark one, or give every item a `type:`. The orchestrator's own form, for an item that reaches it, is `` modification '<name>' names no builder (`type`) and no mod builder is `is_default: true`; name one with `type:` or mark a default ``. |
|
|
582
560
|
| `Executable bash specified for provider bash-remote not found in executables list.` | `validate`; exit 1 | Add a `bash` entry to `cfg/executables.yml` or change the builder's `executable:`. |
|
|
583
561
|
| `bash: binary '/usr/local/bin/bash' not found (declared in cfg/executables.yml; an absolute path, or a name on PATH)` | `validate` and every run; exit 1 | The pinned path is wrong on this machine. Symlink the binary there or fix `binary:`. |
|
|
584
562
|
| ``bash: BashVersionChecker could not parse a version from `/usr/local/bin/bash --version` `` | `validate` and every run | The first line of `--version` has no `version N.N`. Something other than bash is at that path. |
|
|
585
563
|
| `bash 5.3.15 does not meet its requirement <spec> (cfg/executables.yml)` | `validate` and every run | The floor moved or the machine is behind. |
|
|
586
564
|
| `FileNotFoundError: Bash modification script mod_image.sh does not exist or is not a file` | generation; run `ok: false`, traceback in the log | The path is resolved from the configuration root, not from the image file's directory or the shell's. Fix the path or `--root-dir`. |
|
|
587
565
|
| `Bash modification '<name>': ensure declares [...] but every one is empty, so it would emit nothing` | load; CLI exit 1 | Every `ensure` list is empty. Fill one or drop `ensure`. |
|
|
588
|
-
| `Bash modification '<name>': ensure.files[N]: `path` is required` (or `ensure.commands[N]: `run` is required`) | load; CLI exit 1 | A `files` entry without `path`, or a `commands` entry without `run`.
|
|
589
|
-
| `Bash modification '<name>': ensure.packages must be a list of package names, not str 'git'` | load; CLI exit 1 | `packages:` (or `services:`) given as a string
|
|
566
|
+
| `Bash modification '<name>': ensure.files[N]: `path` is required` (or `ensure.commands[N]: `run` is required`) | load; CLI exit 1 | A `files` entry without `path`, or a `commands` entry without `run`. |
|
|
567
|
+
| `Bash modification '<name>': ensure.packages must be a list of package names, not str 'git'` | load; CLI exit 1 | `packages:` (or `services:`) given as a string, which shell would iterate letter by letter. Write a list (`packages: [git]`). |
|
|
590
568
|
| `Bash modification '<name>': ensure.files[N].mode 644 is not a mode YAML could have read from octal` | load; CLI exit 1 | An unquoted `644`, which YAML reads as a decimal number. Write `mode: "0644"`. |
|
|
591
569
|
| `Bash modification '<name>': ensure.files[N]: unknown keys [...]` | load; CLI exit 1 | Only `path`, `content`, `mode` on a file and `run`, `unless` on a command. |
|
|
592
570
|
| `csis ensure: no dnf, yum or apt-get to install <pkg>` | apply / `test-mods` | The image has none of the three package managers. Use a `script` line with the one it has. |
|
|
593
|
-
| `dnf` (or `yum`, `apt-get`) error on a package step | apply / `test-mods` | The first package manager found could not install the package, and its message is the real one (no match, no repository, no network from the build subnet)
|
|
571
|
+
| `dnf` (or `yum`, `apt-get`) error on a package step | apply / `test-mods` | The first package manager found could not install the package, and its message is the real one (no match, no repository, no network from the build subnet); the others are never tried, so no misleading `apt-get: command not found` follows it on an EL host. |
|
|
594
572
|
| `cmp: command not found` or `base64: command not found` | apply | The image lacks diffutils / coreutils. Add the package to an earlier item's `ensure.packages` or a `script` line. |
|
|
595
573
|
| `sudo: a terminal is required to read the password` | apply | The bake ssh user has no passwordless `sudo`. This is an OS builder / runtime matter, not the item's. |
|
|
596
574
|
| `Failed to enable unit: Unit <svc>.service does not exist` | apply | `services:` names a unit the image has not installed yet; install it earlier in the same item's `packages` (packages run before services). |
|
|
@@ -622,20 +600,20 @@ Failures the code raises that have not been seen outside tests:
|
|
|
622
600
|
[abstract_version_checker.py](../base/src/cs_image_system/base/basic/abstract_version_checker.py).
|
|
623
601
|
- The image builder that calls it and hosts its output:
|
|
624
602
|
[packer-plugin](../packer-plugin)
|
|
625
|
-
([
|
|
603
|
+
([packer_image_builder.py](../packer-plugin/src/cs_image_system/packer_plugin/packer_image_builder.py),
|
|
626
604
|
[v2_local_mods.py](../packer-plugin/src/cs_image_system/packer_plugin/v2_local_mods.py)).
|
|
627
605
|
- Lineage records for modifications:
|
|
628
606
|
[lineage.py](../base/src/cs_image_system/base/lineage.py) (`mod_records`);
|
|
629
607
|
the release gate in [release.py](../base/src/cs_image_system/base/release.py).
|
|
630
608
|
- Tests: [tests/test_bash_provisioner.py](tests/test_bash_provisioner.py)
|
|
631
609
|
(two blocks for files plus inline, builder options, script copying);
|
|
632
|
-
[
|
|
610
|
+
[test_modifications.py](../../tests/test_modifications.py)
|
|
633
611
|
(both kinds emitted, unknown type refused, the load rule);
|
|
634
|
-
[
|
|
612
|
+
[test_modifications.py](../../tests/test_modifications.py)
|
|
635
613
|
(`ensure` guards, idempotence recorded, the bundle);
|
|
636
|
-
[
|
|
614
|
+
[test_mod_tests.py](../../tests/test_mod_tests.py)
|
|
637
615
|
(the container tests on the fixture's bash item);
|
|
638
|
-
[
|
|
616
|
+
[test_version_checks.py](../../tests/test_version_checks.py)
|
|
639
617
|
(`BashVersionChecker` found by name, versions judged).
|
|
640
618
|
- No terraform modules and no hashicorp-utils usage: the plugin writes
|
|
641
619
|
Packer provisioner lines directly.
|
{cs_image_system_bash_mod_plugin-0.1.6 → cs_image_system_bash_mod_plugin-0.1.8}/pyproject.toml
RENAMED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cs-image-system-bash-mod-plugin"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.8"
|
|
8
8
|
description = "The shell modification plugin: inline script, script files and an ensure form become Packer shell provisioners"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Mykel Alvis", email = "mykelalvis@infrastructurebuilder.org" }]
|
|
@@ -12,8 +12,8 @@ license = "Apache-2.0"
|
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
requires-python = ">=3.13"
|
|
14
14
|
dependencies = [
|
|
15
|
-
"cs-image-system-contract
|
|
16
|
-
"cs-image-system-base==0.1.
|
|
15
|
+
"cs-image-system-contract>=0.4,<0.5",
|
|
16
|
+
"cs-image-system-base==0.1.8",
|
|
17
17
|
"pydantic>=2.13",
|
|
18
18
|
]
|
|
19
19
|
|
|
@@ -9,17 +9,15 @@ import shutil
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import TypeVar, cast
|
|
11
11
|
|
|
12
|
-
from cs_image_system.contract.assets import AssetSet
|
|
13
|
-
from cs_image_system.base.bake_steps import execute_command_line
|
|
14
|
-
|
|
15
|
-
from .bash_models import BASH_BUILDER, BASH_EXECUTABLE, BashBuilderModel, BashModItemModel, BashModBuilderModel
|
|
16
12
|
from cs_image_system.base.basic.abstract_version_checker import AbstractVersionChecker
|
|
13
|
+
from cs_image_system.base.basic.builder_base_image import ImageBuilderBase, build_target_label_of
|
|
17
14
|
from cs_image_system.base.basic.builder_base_mod import ModBuilderBase
|
|
18
|
-
from cs_image_system.contract.lifecycle import ExecutionLifecyclePhase
|
|
19
15
|
from cs_image_system.base.models.image import Image
|
|
20
|
-
from cs_image_system.base.basic.builder_base_image import ImageBuilderBase
|
|
21
16
|
from cs_image_system.base.models.moditem_type import ModItemModel
|
|
22
|
-
from cs_image_system.
|
|
17
|
+
from cs_image_system.contract.assets import AssetSet
|
|
18
|
+
from cs_image_system.contract.lifecycle import ExecutionLifecyclePhase
|
|
19
|
+
|
|
20
|
+
from .bash_models import BASH_BUILDER, BASH_EXECUTABLE, BashBuilderModel, BashModBuilderModel, BashModItemModel
|
|
23
21
|
|
|
24
22
|
log = logging.getLogger(__name__)
|
|
25
23
|
|
|
@@ -81,14 +79,14 @@ class BashModBuilder(ModBuilderBase[Q]):
|
|
|
81
79
|
for body in parts:
|
|
82
80
|
lines += ['provisioner "shell" {', f' only = ["{build_target_label_of(ibb, image)}"]']
|
|
83
81
|
lines += body
|
|
84
|
-
command = self.model.effective_execute_command() #
|
|
82
|
+
command = self.model.effective_execute_command() # configuration_user
|
|
85
83
|
if command:
|
|
86
84
|
lines.append(f" execute_command = {_hcl_string(command)}")
|
|
87
85
|
else:
|
|
88
|
-
#
|
|
86
|
+
# through the bake's step runner, so `ensure: packages`
|
|
89
87
|
# and a script's own installs wait out a held package database;
|
|
90
88
|
# a builder that says how its script runs keeps its own command
|
|
91
|
-
lines.append(execute_command_line(" "))
|
|
89
|
+
lines.append(self._get_context().services.commands.execute_command_line(" "))
|
|
92
90
|
if self.model.environment_vars:
|
|
93
91
|
lines.append(" environment_vars = [" + ", ".join(_hcl_string(v) for v in self.model.environment_vars) + "]")
|
|
94
92
|
if self.model.expect_disconnect:
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
6
|
from dataclasses import field
|
|
7
|
-
from cs_image_system.contract.model_config import CSIS_MODEL_CONFIG
|
|
8
|
-
from pydantic.dataclasses import dataclass # stage 23: validation at construction
|
|
9
7
|
from pathlib import Path
|
|
10
8
|
from typing import Any
|
|
11
9
|
|
|
10
|
+
from pydantic.dataclasses import dataclass # validation at construction
|
|
11
|
+
|
|
12
12
|
from cs_image_system.base.models.mod_builder import ModBuilderModel
|
|
13
13
|
from cs_image_system.base.models.moditem_type import ModItemModel
|
|
14
|
+
from cs_image_system.contract.model_config import CSIS_MODEL_CONFIG
|
|
15
|
+
|
|
14
16
|
BASH_EXECUTABLE: str = "bash"
|
|
15
17
|
|
|
16
18
|
BASH_BUILDER: str = "bash-remote"
|
|
@@ -31,8 +33,8 @@ def _mode_text(mode: Any) -> str:
|
|
|
31
33
|
"""A file mode as the four-digit octal text ``install -m`` takes.
|
|
32
34
|
|
|
33
35
|
YAML reads an unquoted ``0644`` as the integer 420, so an integer is
|
|
34
|
-
rendered back in octal
|
|
35
|
-
|
|
36
|
+
rendered back in octal; a string must already be three or four octal
|
|
37
|
+
digits."""
|
|
36
38
|
if isinstance(mode, bool):
|
|
37
39
|
raise ValueError(f"mode {mode!r} is not a file mode")
|
|
38
40
|
if isinstance(mode, int):
|
|
@@ -74,8 +76,8 @@ def _entries(label: str, kind: str, value: Any, allowed: set[str], required: str
|
|
|
74
76
|
|
|
75
77
|
|
|
76
78
|
def validate_ensure(label: str, ensure: Any) -> dict[str, Any]:
|
|
77
|
-
"""Every ``ensure`` entry checked at load, one rule per kind
|
|
78
|
-
|
|
79
|
+
"""Every ``ensure`` entry checked at load, one rule per kind. A valid
|
|
80
|
+
mapping comes back UNCHANGED except that an integer
|
|
79
81
|
file mode becomes its octal text: the lineage fingerprint hashes this
|
|
80
82
|
mapping, so a tree that was already valid keeps every fingerprint.
|
|
81
83
|
|
|
@@ -171,7 +173,7 @@ class BashModItemModel(ModItemModel):
|
|
|
171
173
|
pkgs = [str(p) for p in self.ensure.get("packages", []) or []]
|
|
172
174
|
if pkgs:
|
|
173
175
|
joined = " ".join(pkgs)
|
|
174
|
-
#
|
|
176
|
+
# the FIRST package manager that exists installs,
|
|
175
177
|
# and its failure is the failure (the old chain fell through dnf,
|
|
176
178
|
# yum and apt-get, so an EL host showed `apt-get: command not found`)
|
|
177
179
|
out.append(f"for p in {joined}; do rpm -q \"$p\" >/dev/null 2>&1 || dpkg -s \"$p\" >/dev/null 2>&1 || "
|
|
@@ -202,7 +204,7 @@ class BashBuilderModel(ModBuilderModel):
|
|
|
202
204
|
"""Dataclass representing a Bash script modification.
|
|
203
205
|
"""
|
|
204
206
|
type = BASH_BUILDER
|
|
205
|
-
#
|
|
207
|
+
# when set, every item's lines run as this user -- the shell
|
|
206
208
|
# provisioner's execute_command becomes `sudo su - <user> -c '...'`, so the
|
|
207
209
|
# bake user only launches them. Declaring it with execute_command is
|
|
208
210
|
# refused (one of the two decides how the script runs).
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
-
|
|
7
|
-
from .bash_models import BASH_BUILDER, BASH_EXECUTABLE, BashModItemModel, BashModBuilderModel
|
|
6
|
+
|
|
8
7
|
from cs_image_system.base.basic.abstract_plugin_metadata import AbstractPluginMetadata
|
|
9
8
|
|
|
9
|
+
from .bash_builder import BashModBuilder, BashVersionChecker
|
|
10
|
+
from .bash_models import BASH_BUILDER, BASH_EXECUTABLE, BashModBuilderModel, BashModItemModel
|
|
11
|
+
|
|
10
12
|
log = logging.getLogger(__name__)
|
|
11
13
|
|
|
12
14
|
class BashModTypes(AbstractPluginMetadata):
|
|
@@ -7,26 +7,34 @@ from pathlib import Path
|
|
|
7
7
|
from types import SimpleNamespace
|
|
8
8
|
from typing import Any, cast
|
|
9
9
|
|
|
10
|
+
from cs_image_system.base.bake_steps import execute_command_line
|
|
10
11
|
from cs_image_system.bash_mod_plugin.bash_builder import BashModBuilder
|
|
11
12
|
from cs_image_system.bash_mod_plugin.bash_models import BashModItemModel
|
|
12
|
-
from cs_image_system.
|
|
13
|
+
from cs_image_system.contract.lifecycle import ExecutionLifecyclePhase
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def _builder(**model_fields):
|
|
16
17
|
fields = {"execute_command": None, "environment_vars": [], "expect_disconnect": False}
|
|
17
18
|
fields.update(model_fields)
|
|
18
19
|
model = SimpleNamespace(**fields)
|
|
19
|
-
#
|
|
20
|
+
# the builder asks the model how the script runs
|
|
20
21
|
model.effective_execute_command = lambda: model.execute_command
|
|
21
22
|
b = BashModBuilder.__new__(BashModBuilder)
|
|
22
23
|
b._model = model # type: ignore[attr-defined]
|
|
24
|
+
# the builder asks its run's commands service for the execute_command
|
|
25
|
+
# line; a builder made here is bound to a stand-in that answers with the
|
|
26
|
+
# kit's own (the loader binds a real one)
|
|
27
|
+
b.bind(cast(Any, SimpleNamespace(services=SimpleNamespace(
|
|
28
|
+
commands=SimpleNamespace(execute_command_line=execute_command_line)))))
|
|
23
29
|
return b
|
|
24
30
|
|
|
25
31
|
|
|
26
32
|
def _emit(builder, mod):
|
|
27
33
|
image = SimpleNamespace(name="img")
|
|
34
|
+
# the image builder labels the build target through its runtime; a stub answers here
|
|
35
|
+
ibb = SimpleNamespace(build_target_label=lambda img: f"amazon-ebs.{img.name}")
|
|
28
36
|
items = builder.generate_items_during_modification(
|
|
29
|
-
image, mod, cast(Any,
|
|
37
|
+
image, mod, cast(Any, ibb), ExecutionLifecyclePhase.IMAGE_GENERATION, Path("build.pkr.hcl"))
|
|
30
38
|
return "\n".join(a.value for a in items)
|
|
31
39
|
|
|
32
40
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|