cs-image-system-bash-mod-plugin 0.1.1__py3-none-any.whl
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/__init__.py +3 -0
- cs_image_system/bash_mod_plugin/bash_builder.py +136 -0
- cs_image_system/bash_mod_plugin/bash_models.py +238 -0
- cs_image_system/bash_mod_plugin/main.py +31 -0
- cs_image_system/py.typed +0 -0
- cs_image_system_bash_mod_plugin-0.1.1.dist-info/METADATA +656 -0
- cs_image_system_bash_mod_plugin-0.1.1.dist-info/RECORD +10 -0
- cs_image_system_bash_mod_plugin-0.1.1.dist-info/WHEEL +4 -0
- cs_image_system_bash_mod_plugin-0.1.1.dist-info/entry_points.txt +2 -0
- cs_image_system_bash_mod_plugin-0.1.1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TypeVar, cast
|
|
11
|
+
|
|
12
|
+
from cs_image_system.base.basic.asset 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
|
+
from cs_image_system.base.basic.abstract_version_checker import AbstractVersionChecker
|
|
17
|
+
from cs_image_system.base.basic.builder_base_mod import ModBuilderBase
|
|
18
|
+
from cs_image_system.base.lifecycle import ExecutionLifecyclePhase
|
|
19
|
+
from cs_image_system.base.models.image import Image
|
|
20
|
+
from cs_image_system.base.basic.builder_base_image import ImageBuilderBase
|
|
21
|
+
from cs_image_system.base.models.moditem_type import ModItemModel
|
|
22
|
+
from cs_image_system.base.basic.builder_base_image import build_target_label_of
|
|
23
|
+
|
|
24
|
+
log = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
Q = TypeVar("Q", bound=BashModBuilderModel)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _hcl_string(s: str) -> str:
|
|
31
|
+
"""HCL2 string literal; ${ and %{ are escaped so shell lines like
|
|
32
|
+
dpkg-query -f='${Package}' survive packer's template parser."""
|
|
33
|
+
t = s.replace("\\", "\\\\").replace('"', '\\"')
|
|
34
|
+
return '"' + t.replace("${", "$${").replace("%{", "%%{") + '"'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BashModBuilder(ModBuilderBase[Q]):
|
|
38
|
+
"""Bash-based modification builder (DESIGN §3M2).
|
|
39
|
+
|
|
40
|
+
Emits well-formed packer ``provisioner "shell"`` blocks scoped to the
|
|
41
|
+
image: script files (copied beside the packer root) as ``scripts = [...]``
|
|
42
|
+
and inline lines as ``inline = [...]`` -- as two blocks when both are
|
|
43
|
+
given, because packer forbids both arguments in one provisioner.
|
|
44
|
+
"""
|
|
45
|
+
@classmethod
|
|
46
|
+
def csis_name(cls) -> str:
|
|
47
|
+
return BASH_BUILDER
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def model(self) -> BashBuilderModel: # type: ignore
|
|
52
|
+
return cast(BashBuilderModel, self._model)
|
|
53
|
+
|
|
54
|
+
def generate_items_before_modification(self, image: Image, mod: ModItemModel,
|
|
55
|
+
ibb: ImageBuilderBase,
|
|
56
|
+
phase: ExecutionLifecyclePhase,
|
|
57
|
+
path: Path | None = None) -> AssetSet:
|
|
58
|
+
return AssetSet()
|
|
59
|
+
|
|
60
|
+
def generate_items_during_modification(self, image: Image, mod: BashModItemModel, # pyright: ignore[reportIncompatibleMethodOverride]
|
|
61
|
+
ibb: ImageBuilderBase,
|
|
62
|
+
phase: ExecutionLifecyclePhase,
|
|
63
|
+
path: Path | None = None) -> AssetSet:
|
|
64
|
+
retval = AssetSet()
|
|
65
|
+
if path is None:
|
|
66
|
+
path = self.get_path_for_phase(phase)
|
|
67
|
+
ensured: list[str] = list(mod.ensure_lines()) if isinstance(mod, BashModItemModel) else []
|
|
68
|
+
inline: list[str] = ensured + [str(l) for l in (getattr(mod, "script", None) or [])]
|
|
69
|
+
scripts = list(getattr(mod, "scripts", None) or [])
|
|
70
|
+
if not inline and not scripts:
|
|
71
|
+
log.warning(f"Bash modification {mod.get_display_name()} has no script lines or files; nothing emitted")
|
|
72
|
+
return retval
|
|
73
|
+
# Packer's shell provisioner takes EITHER `scripts` OR `inline`, never
|
|
74
|
+
# both: a mod with both emits two blocks, script files first.
|
|
75
|
+
lines = [f"# Modifications for {mod.name} of type {mod.type_} (shell)"]
|
|
76
|
+
parts: list[list[str]] = []
|
|
77
|
+
if scripts:
|
|
78
|
+
parts.append([" scripts = [" + ", ".join(_hcl_string(s) for s in scripts) + "]"])
|
|
79
|
+
if inline:
|
|
80
|
+
parts.append([" inline = ["] + [f" {_hcl_string(l)}," for l in inline] + [" ]"])
|
|
81
|
+
for body in parts:
|
|
82
|
+
lines += ['provisioner "shell" {', f' only = ["{build_target_label_of(ibb, image)}"]']
|
|
83
|
+
lines += body
|
|
84
|
+
command = self.model.effective_execute_command() # stage 63: configuration_user
|
|
85
|
+
if command:
|
|
86
|
+
lines.append(f" execute_command = {_hcl_string(command)}")
|
|
87
|
+
else:
|
|
88
|
+
# stage 86: through the bake's step runner, so `ensure: packages`
|
|
89
|
+
# and a script's own installs wait out a held package database;
|
|
90
|
+
# a builder that says how its script runs keeps its own command
|
|
91
|
+
lines.append(execute_command_line(" "))
|
|
92
|
+
if self.model.environment_vars:
|
|
93
|
+
lines.append(" environment_vars = [" + ", ".join(_hcl_string(v) for v in self.model.environment_vars) + "]")
|
|
94
|
+
if self.model.expect_disconnect:
|
|
95
|
+
lines.append(" expect_disconnect = true")
|
|
96
|
+
lines.append("}\n")
|
|
97
|
+
for l in lines:
|
|
98
|
+
retval.add(path, l)
|
|
99
|
+
return retval
|
|
100
|
+
|
|
101
|
+
def generate_items_after_modification(self, image: Image, mod: ModItemModel,
|
|
102
|
+
ibb: ImageBuilderBase,
|
|
103
|
+
phase: ExecutionLifecyclePhase,
|
|
104
|
+
path: Path | None = None) -> AssetSet:
|
|
105
|
+
return AssetSet()
|
|
106
|
+
|
|
107
|
+
def copy_external_assets(self, target_path: Path, mod: ModItemModel | None = None) -> dict[str, Path]:
|
|
108
|
+
"""Copy the item's script files beside the packer root (cwd-relative
|
|
109
|
+
sources, like the ansible builder's playbooks) and return the mapping
|
|
110
|
+
the item uses to remap itself."""
|
|
111
|
+
assets: dict[str, Path] = {}
|
|
112
|
+
scripts = list(getattr(mod, "scripts", None) or []) if mod is not None else []
|
|
113
|
+
for script in scripts:
|
|
114
|
+
source = Path(script)
|
|
115
|
+
if source.is_absolute():
|
|
116
|
+
assets[script] = source
|
|
117
|
+
continue
|
|
118
|
+
if not source.is_file():
|
|
119
|
+
raise FileNotFoundError(f"Bash modification script {source} does not exist or is not a file")
|
|
120
|
+
target = target_path / source
|
|
121
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
shutil.copy2(source, target)
|
|
123
|
+
assets[script] = source
|
|
124
|
+
return assets
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class BashVersionChecker(AbstractVersionChecker):
|
|
128
|
+
"""Version checker for Bash."""
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def csis_name(cls) -> str:
|
|
132
|
+
return BASH_EXECUTABLE
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_regex(self) -> str:
|
|
136
|
+
return r".*version\s+([\d\.]+)"
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import field
|
|
7
|
+
from cs_image_system.base.models.model_config import CSIS_MODEL_CONFIG
|
|
8
|
+
from pydantic.dataclasses import dataclass # stage 23: validation at construction
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from cs_image_system.base.models.mod_builder import ModBuilderModel
|
|
13
|
+
from cs_image_system.base.models.moditem_type import ModItemModel
|
|
14
|
+
BASH_EXECUTABLE: str = "bash"
|
|
15
|
+
|
|
16
|
+
BASH_BUILDER: str = "bash-remote"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
ENSURE_KINDS: dict[str, str] = {
|
|
20
|
+
"packages": "a list of package names",
|
|
21
|
+
"files": "a list of {path, content, mode} mappings",
|
|
22
|
+
"services": "a list of systemd unit names",
|
|
23
|
+
"commands": "a list of {run, unless} mappings",
|
|
24
|
+
}
|
|
25
|
+
_FILE_KEYS = {"path", "content", "mode"}
|
|
26
|
+
_COMMAND_KEYS = {"run", "unless"}
|
|
27
|
+
_MODE_TEXT = re.compile(r"^[0-7]{3,4}$")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _mode_text(mode: Any) -> str:
|
|
31
|
+
"""A file mode as the four-digit octal text ``install -m`` takes.
|
|
32
|
+
|
|
33
|
+
YAML reads an unquoted ``0644`` as the integer 420, so an integer is
|
|
34
|
+
rendered back in octal (stage 63 item 16, decided 2026-09-25); a
|
|
35
|
+
string must already be three or four octal digits."""
|
|
36
|
+
if isinstance(mode, bool):
|
|
37
|
+
raise ValueError(f"mode {mode!r} is not a file mode")
|
|
38
|
+
if isinstance(mode, int):
|
|
39
|
+
return f"{mode:04o}"
|
|
40
|
+
return str(mode)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _names(label: str, kind: str, value: Any) -> list[str]:
|
|
44
|
+
if not isinstance(value, list):
|
|
45
|
+
raise ValueError(f"{label}: ensure.{kind} must be {ENSURE_KINDS[kind]}, "
|
|
46
|
+
f"not {type(value).__name__} {value!r} (write `{kind}: [{value}]` for one)")
|
|
47
|
+
out: list[str] = []
|
|
48
|
+
for i, v in enumerate(value):
|
|
49
|
+
if not isinstance(v, str) or not v.strip():
|
|
50
|
+
raise ValueError(f"{label}: ensure.{kind}[{i}] must be a non-empty name, not {v!r}")
|
|
51
|
+
out.append(v)
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _entries(label: str, kind: str, value: Any, allowed: set[str], required: str) -> list[dict[str, Any]]:
|
|
56
|
+
if not isinstance(value, list):
|
|
57
|
+
raise ValueError(f"{label}: ensure.{kind} must be {ENSURE_KINDS[kind]}, not {type(value).__name__}")
|
|
58
|
+
for i, entry in enumerate(value):
|
|
59
|
+
where = f"{label}: ensure.{kind}[{i}]"
|
|
60
|
+
if not isinstance(entry, dict):
|
|
61
|
+
raise ValueError(f"{where} must be a mapping ({ENSURE_KINDS[kind]}), not {entry!r}")
|
|
62
|
+
unknown = set(entry) - allowed
|
|
63
|
+
if unknown:
|
|
64
|
+
raise ValueError(f"{where}: unknown keys {sorted(unknown)} (allowed: {sorted(allowed)})")
|
|
65
|
+
req = entry.get(required)
|
|
66
|
+
if not isinstance(req, str) or not req.strip():
|
|
67
|
+
raise ValueError(f"{where}: `{required}` is required and must be non-empty text")
|
|
68
|
+
for k, v in entry.items():
|
|
69
|
+
if k in (required, "mode"):
|
|
70
|
+
continue
|
|
71
|
+
if v is not None and not isinstance(v, str):
|
|
72
|
+
raise ValueError(f"{where}: `{k}` must be text, not {type(v).__name__} {v!r}")
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def validate_ensure(label: str, ensure: Any) -> dict[str, Any]:
|
|
77
|
+
"""Every ``ensure`` entry checked at load, one rule per kind (stage 63
|
|
78
|
+
item 16). A valid mapping comes back UNCHANGED except that an integer
|
|
79
|
+
file mode becomes its octal text: the lineage fingerprint hashes this
|
|
80
|
+
mapping, so a tree that was already valid keeps every fingerprint.
|
|
81
|
+
|
|
82
|
+
Refused: an unknown kind; a kind that is not a list (``packages: git``
|
|
83
|
+
iterated the letters); an entry of the wrong shape or with unknown keys
|
|
84
|
+
(a missing ``path`` or ``run`` was a ``KeyError`` at generation); a mode
|
|
85
|
+
that is not three or four octal digits; an integer mode above 0777 (an
|
|
86
|
+
unquoted ``644`` is the decimal number, not the mode); and an
|
|
87
|
+
``ensure`` whose kinds are all empty (it loaded as ``idempotent:
|
|
88
|
+
declared`` and emitted nothing)."""
|
|
89
|
+
label = f"Bash modification '{label}'"
|
|
90
|
+
if ensure is None:
|
|
91
|
+
return {}
|
|
92
|
+
if not isinstance(ensure, dict):
|
|
93
|
+
raise ValueError(f"{label}: ensure must be a mapping of {sorted(ENSURE_KINDS)}, not {ensure!r}")
|
|
94
|
+
ensure = dict(ensure)
|
|
95
|
+
if not ensure:
|
|
96
|
+
return {}
|
|
97
|
+
unknown = set(ensure) - set(ENSURE_KINDS)
|
|
98
|
+
if unknown:
|
|
99
|
+
raise ValueError(f"{label}: unknown ensure keys {sorted(unknown)} (allowed: {sorted(ENSURE_KINDS)})")
|
|
100
|
+
for kind in ("packages", "services"):
|
|
101
|
+
if ensure.get(kind) is not None:
|
|
102
|
+
_names(label, kind, ensure[kind])
|
|
103
|
+
if ensure.get("files") is not None:
|
|
104
|
+
files = _entries(label, "files", ensure["files"], _FILE_KEYS, "path")
|
|
105
|
+
fixed: list[dict[str, Any]] = []
|
|
106
|
+
for i, f in enumerate(files):
|
|
107
|
+
if "mode" in f and f["mode"] is not None:
|
|
108
|
+
mode = f["mode"]
|
|
109
|
+
if isinstance(mode, int) and not isinstance(mode, bool) and not 0 <= mode <= 0o777:
|
|
110
|
+
raise ValueError(f"{label}: ensure.files[{i}].mode {mode} is not a mode YAML could have "
|
|
111
|
+
f"read from octal (an unquoted `644` is the decimal number); quote it: \"0644\"")
|
|
112
|
+
text = _mode_text(mode)
|
|
113
|
+
if not _MODE_TEXT.match(text):
|
|
114
|
+
raise ValueError(f"{label}: ensure.files[{i}].mode {mode!r} must be three or four octal digits")
|
|
115
|
+
f = {**f, "mode": text} if isinstance(mode, int) else f
|
|
116
|
+
fixed.append(f)
|
|
117
|
+
ensure["files"] = fixed
|
|
118
|
+
if ensure.get("commands") is not None:
|
|
119
|
+
_entries(label, "commands", ensure["commands"], _COMMAND_KEYS, "run")
|
|
120
|
+
if not any(ensure.get(k) for k in ENSURE_KINDS):
|
|
121
|
+
raise ValueError(f"{label}: ensure declares {sorted(ensure)} but every one is empty, so it would "
|
|
122
|
+
f"emit nothing; remove `ensure` or give it an entry")
|
|
123
|
+
return ensure
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(kw_only=True, config=CSIS_MODEL_CONFIG)
|
|
127
|
+
class BashModItemModel(ModItemModel):
|
|
128
|
+
"""A shell-script modification (DESIGN §3M2).
|
|
129
|
+
|
|
130
|
+
Attributes:
|
|
131
|
+
script: inline command lines, run in order inside one
|
|
132
|
+
``provisioner "shell"`` (``inline = [...]``).
|
|
133
|
+
scripts: script FILES (config-root-relative), copied beside the
|
|
134
|
+
packer root and run as ``scripts = [...]``.
|
|
135
|
+
ensure: a declarative, IDEMPOTENT form (EXPLORE "Generation of
|
|
136
|
+
Idempotent Script-based Modifications") the plugin turns into
|
|
137
|
+
guarded shell: ``packages: [..]``, ``files: [{path, content,
|
|
138
|
+
mode}]``, ``services: [..]`` (enabled + started), ``commands:
|
|
139
|
+
[{run, unless}]`` (run only when ``unless`` fails).
|
|
140
|
+
At least one of the three must be given. Free-form ``script``/``scripts``
|
|
141
|
+
are recorded as ``idempotent: unknown``; ``ensure``-only items as
|
|
142
|
+
``idempotent: declared``.
|
|
143
|
+
"""
|
|
144
|
+
type = BASH_BUILDER
|
|
145
|
+
script: list[str] = field(default_factory=list)
|
|
146
|
+
scripts: list[str] = field(default_factory=list)
|
|
147
|
+
ensure: dict[str, Any] = field(default_factory=dict)
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def csis_name(cls) -> str:
|
|
151
|
+
return BASH_BUILDER
|
|
152
|
+
|
|
153
|
+
def __post_init__(self) -> None:
|
|
154
|
+
super().__post_init__()
|
|
155
|
+
self.script = [str(s) for s in (self.script or []) if str(s).strip()]
|
|
156
|
+
self.scripts = [str(s) for s in (self.scripts or []) if str(s).strip()]
|
|
157
|
+
self.ensure = validate_ensure(self.get_display_name(), self.ensure)
|
|
158
|
+
if not self.script and not self.scripts and not self.ensure:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
f"Bash modification '{self.get_display_name()}' must supply 'script' (inline lines), "
|
|
161
|
+
"'scripts' (script files) and/or 'ensure' (declarative, idempotent)")
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def idempotent(self) -> str:
|
|
165
|
+
return "declared" if self.ensure and not self.script and not self.scripts else "unknown"
|
|
166
|
+
|
|
167
|
+
def ensure_lines(self) -> list[str]:
|
|
168
|
+
"""Guarded shell for the declarative form: every step checks before it
|
|
169
|
+
changes, so re-running is a no-op."""
|
|
170
|
+
out: list[str] = []
|
|
171
|
+
pkgs = [str(p) for p in self.ensure.get("packages", []) or []]
|
|
172
|
+
if pkgs:
|
|
173
|
+
joined = " ".join(pkgs)
|
|
174
|
+
# stage 63 item 16: the FIRST package manager that exists installs,
|
|
175
|
+
# and its failure is the failure (the old chain fell through dnf,
|
|
176
|
+
# yum and apt-get, so an EL host showed `apt-get: command not found`)
|
|
177
|
+
out.append(f"for p in {joined}; do rpm -q \"$p\" >/dev/null 2>&1 || dpkg -s \"$p\" >/dev/null 2>&1 || "
|
|
178
|
+
"if command -v dnf >/dev/null 2>&1; then sudo dnf -y install \"$p\"; "
|
|
179
|
+
"elif command -v yum >/dev/null 2>&1; then sudo yum -y install \"$p\"; "
|
|
180
|
+
"elif command -v apt-get >/dev/null 2>&1; then sudo apt-get install -y \"$p\"; "
|
|
181
|
+
"else echo \"csis ensure: no dnf, yum or apt-get to install $p\" >&2; false; fi || exit 1; done")
|
|
182
|
+
for f in self.ensure.get("files", []) or []:
|
|
183
|
+
path = str(f["path"]); content = str(f.get("content", "")); mode = _mode_text(f.get("mode", "0644"))
|
|
184
|
+
b64 = __import__("base64").b64encode(content.encode()).decode()
|
|
185
|
+
out.append(f"printf '%s' '{b64}' | base64 -d > /tmp/.csis-ensure && "
|
|
186
|
+
f"( sudo cmp -s /tmp/.csis-ensure '{path}' || sudo install -m {mode} /tmp/.csis-ensure '{path}' ) && rm -f /tmp/.csis-ensure")
|
|
187
|
+
for svc in self.ensure.get("services", []) or []:
|
|
188
|
+
out.append(f"sudo systemctl is-enabled '{svc}' >/dev/null 2>&1 || sudo systemctl enable '{svc}'")
|
|
189
|
+
out.append(f"sudo systemctl is-active '{svc}' >/dev/null 2>&1 || sudo systemctl start '{svc}'")
|
|
190
|
+
for c in self.ensure.get("commands", []) or []:
|
|
191
|
+
run = str(c["run"]); unless = c.get("unless")
|
|
192
|
+
out.append(f"( {unless} ) >/dev/null 2>&1 || {{ {run}; }}" if unless else run)
|
|
193
|
+
return out
|
|
194
|
+
|
|
195
|
+
def remap_self_with_copied_assets(self, copied_assets: dict[str, Path]) -> None:
|
|
196
|
+
"""Point script files at their copies beside the packer root."""
|
|
197
|
+
self.scripts = [str(copied_assets.get(s, s)) for s in self.scripts]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dataclass(kw_only=True, config=CSIS_MODEL_CONFIG)
|
|
201
|
+
class BashBuilderModel(ModBuilderModel):
|
|
202
|
+
"""Dataclass representing a Bash script modification.
|
|
203
|
+
"""
|
|
204
|
+
type = BASH_BUILDER
|
|
205
|
+
# stage 63: when set, every item's lines run as this user -- the shell
|
|
206
|
+
# provisioner's execute_command becomes `sudo su - <user> -c '...'`, so the
|
|
207
|
+
# bake user only launches them. Declaring it with execute_command is
|
|
208
|
+
# refused (one of the two decides how the script runs).
|
|
209
|
+
configuration_user: str | None = None
|
|
210
|
+
# Optional packer shell-provisioner settings applied to every item.
|
|
211
|
+
execute_command: str | None = None
|
|
212
|
+
environment_vars: list[str] = field(default_factory=list)
|
|
213
|
+
expect_disconnect: bool = False
|
|
214
|
+
|
|
215
|
+
def __post_init__(self) -> None:
|
|
216
|
+
super().__post_init__()
|
|
217
|
+
if self.configuration_user and self.execute_command:
|
|
218
|
+
raise ValueError(f"mod builder {self.name!r}: `configuration_user` and `execute_command` both say "
|
|
219
|
+
"how the script runs; declare one")
|
|
220
|
+
|
|
221
|
+
def effective_execute_command(self) -> str | None:
|
|
222
|
+
"""The shell provisioner's execute_command: the declared one, else,
|
|
223
|
+
with a configuration_user, packer's default command run as that user
|
|
224
|
+
through `su -` (its environment variables travel inside the command)."""
|
|
225
|
+
if self.execute_command:
|
|
226
|
+
return self.execute_command
|
|
227
|
+
if self.configuration_user:
|
|
228
|
+
return (f"chmod +x {{{{ .Path }}}}; sudo su - {self.configuration_user} "
|
|
229
|
+
f"-c '{{{{ .Vars }}}} {{{{ .Path }}}}'")
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass(kw_only=True, config=CSIS_MODEL_CONFIG)
|
|
234
|
+
class BashModBuilderModel(BashBuilderModel):
|
|
235
|
+
|
|
236
|
+
@classmethod
|
|
237
|
+
def csis_name(cls) -> str:
|
|
238
|
+
return BASH_BUILDER
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from .bash_builder import BashModBuilder, BashVersionChecker
|
|
7
|
+
from .bash_models import BASH_BUILDER, BASH_EXECUTABLE, BashModItemModel, BashModBuilderModel
|
|
8
|
+
from cs_image_system.base.basic.abstract_plugin_metadata import AbstractPluginMetadata
|
|
9
|
+
from cs_image_system.base.protocols.plugin_metadata import PluginMetadataProtocol
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
class BashModTypes(AbstractPluginMetadata, PluginMetadataProtocol):
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
super().__init__( "1", "3.13",
|
|
16
|
+
{BASH_BUILDER: [
|
|
17
|
+
BashModBuilderModel,
|
|
18
|
+
BashModBuilder,
|
|
19
|
+
BashModItemModel
|
|
20
|
+
],
|
|
21
|
+
BASH_EXECUTABLE: [
|
|
22
|
+
BashVersionChecker,
|
|
23
|
+
]},
|
|
24
|
+
{BashModBuilderModel: BashModBuilder}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def initialize():
|
|
29
|
+
log.debug("Initializing Bash Provider")
|
|
30
|
+
return BashModTypes()
|
|
31
|
+
|
cs_image_system/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cs-image-system-bash-mod-plugin
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: The shell modification plugin: inline script, script files and an ensure form become Packer shell provisioners
|
|
5
|
+
Project-URL: Repository, https://github.com/infrastructurebuilder/cs-image-system-3
|
|
6
|
+
Project-URL: Documentation, https://github.com/infrastructurebuilder/cs-image-system-3/blob/develop/docs/OPERATIONS.md
|
|
7
|
+
Project-URL: Package README, https://github.com/infrastructurebuilder/cs-image-system-3/blob/develop/packages/bash-mod-plugin/README.md
|
|
8
|
+
Author-email: Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Python: >=3.13
|
|
12
|
+
Requires-Dist: cs-image-system-system==0.1.1
|
|
13
|
+
Requires-Dist: pydantic>=2.13
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# cs-image-system-bash-mod-plugin
|
|
17
|
+
|
|
18
|
+
The shell modification plugin. It registers one modification builder,
|
|
19
|
+
`bash-remote`, and the modification item model that goes with it. A
|
|
20
|
+
modification item under an image's `modifications:` carries inline shell
|
|
21
|
+
lines (`script`), script files (`scripts`) and/or a declarative, idempotent
|
|
22
|
+
form (`ensure`); the builder turns them into `provisioner "shell"` blocks in
|
|
23
|
+
the image builder's Packer build file, scoped to that one image. The plugin
|
|
24
|
+
owns no files, roots or commands of its own: everything it produces lands in
|
|
25
|
+
the Packer root the image builder writes and runs when that root's
|
|
26
|
+
`packer build .` runs.
|
|
27
|
+
|
|
28
|
+
## What it registers
|
|
29
|
+
|
|
30
|
+
Entry point ([pyproject.toml](pyproject.toml)):
|
|
31
|
+
|
|
32
|
+
```toml
|
|
33
|
+
[project.entry-points."cs_image_system.plugins.modification"]
|
|
34
|
+
bash_plugin = "cs_image_system.bash_mod_plugin.main:initialize"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`initialize()` ([main.py](src/cs_image_system/bash_mod_plugin/main.py))
|
|
38
|
+
returns a `BashModTypes` metadata object (metadata version `1`, Python
|
|
39
|
+
`3.13`) with two service keys:
|
|
40
|
+
|
|
41
|
+
| Service key | Classes | Classification (VCT) |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `bash-remote` | `BashModBuilderModel`, `BashModBuilder`, `BashModItemModel` | `MOD_BUILDER_MODEL` / `MOD_BUILDER` / `MOD_BUILDER_ITEM_MODEL` |
|
|
44
|
+
| `bash` | `BashVersionChecker` | `VERSION_CHECKER` |
|
|
45
|
+
|
|
46
|
+
How a YAML entry selects these classes:
|
|
47
|
+
|
|
48
|
+
- A `mod_builders:` entry uses `type: bash-remote` (no aliases on the
|
|
49
|
+
type key) to get a `BashModBuilderModel` and its `BashModBuilder`.
|
|
50
|
+
- A modification item under an image's `modifications:` uses `type:` to
|
|
51
|
+
name a *builder*: the builder's `name`, one of its `aliases:`, or
|
|
52
|
+
`default`. The orchestrator
|
|
53
|
+
([orchestrator.py](../base/src/cs_image_system/base/orchestrator.py))
|
|
54
|
+
looks the builder up by name or alias, reads the builder's own `type`
|
|
55
|
+
(`bash-remote`) and loads the item into the class registered under that
|
|
56
|
+
service key for `MOD_BUILDER_ITEM_MODEL`: `BashModItemModel`. The
|
|
57
|
+
registry alone decides; the builder model's
|
|
58
|
+
`get_target_deferred_type_by_VCT` method, which also named that class
|
|
59
|
+
and had no caller, is gone (stage 63, 2026-09-25), as is the plugin's
|
|
60
|
+
empty `helpers.py`.
|
|
61
|
+
- **The item's `type` is rewritten to the builder's `name` at load**
|
|
62
|
+
(stage 63 item 15). The image builder later fetches the builder with
|
|
63
|
+
`ctx.mod_builders.get(mod.get_type())`, a map keyed by builder NAME
|
|
64
|
+
only, so an alias must not survive the load. An item declared
|
|
65
|
+
`type: bash` (the fixture builder's alias) is loaded as
|
|
66
|
+
`type: bash-remote`. Before 2026-09-25 the alias was kept, and
|
|
67
|
+
generation stopped with
|
|
68
|
+
`AssertionError: No mod builder bash found in context for <image>`.
|
|
69
|
+
- An `executables:` entry named `bash` is version-checked by
|
|
70
|
+
`BashVersionChecker` (regex `.*version\s+([\d\.]+)` over
|
|
71
|
+
`bash --version`).
|
|
72
|
+
|
|
73
|
+
## Models
|
|
74
|
+
|
|
75
|
+
### `BashBuilderModel` and `BashModBuilderModel` (`bash-remote`)
|
|
76
|
+
|
|
77
|
+
Defined in [bash_models.py](src/cs_image_system/bash_mod_plugin/bash_models.py).
|
|
78
|
+
`BashBuilderModel` extends
|
|
79
|
+
[`ModBuilderModel`](../base/src/cs_image_system/base/models/mod_builder.py),
|
|
80
|
+
which adds nothing to
|
|
81
|
+
[`BuilderModel`](../base/src/cs_image_system/base/models/builder_model.py)
|
|
82
|
+
(`name`, `type`, `description`, `aliases`, `executable`, `is_default`,
|
|
83
|
+
`config`, `gitignore`, `tags`). `BashModBuilderModel` is the registered
|
|
84
|
+
subclass; it adds no fields.
|
|
85
|
+
|
|
86
|
+
| Field | Type | Default | Meaning |
|
|
87
|
+
|---|---|---|---|
|
|
88
|
+
| `execute_command` | `str \| None` | `None` | Emitted as `execute_command = "..."` on every shell provisioner of every item (Packer's command template, for example `sudo -E bash '{{.Path}}'`). The way to change how the script is invoked. May not be declared together with `configuration_user`. |
|
|
89
|
+
| `configuration_user` | `str \| None` | `None` | When set, every shell provisioner of this builder gets `execute_command = "chmod +x {{ .Path }}; sudo su - <user> -c '{{ .Vars }} {{ .Path }}'"`, so each item's lines run as that user; the bake user only launches them (stage 63; declared and not read until 2026-09-25). Declaring it together with `execute_command` is refused at load. |
|
|
90
|
+
| `environment_vars` | `list[str]` | `[]` | Emitted as `environment_vars = [...]` on every shell provisioner. |
|
|
91
|
+
| `expect_disconnect` | `bool` | `False` | Emits `expect_disconnect = true` on every shell provisioner. |
|
|
92
|
+
|
|
93
|
+
`extra_arguments` is not a field (stage 63): it was declared and never
|
|
94
|
+
read, so it is removed, and a builder that still declares it fails to load
|
|
95
|
+
with the unknown-key refusal. Use `execute_command` to change the
|
|
96
|
+
invocation.
|
|
97
|
+
|
|
98
|
+
Base fields this plugin constrains: `executable` should name the `bash`
|
|
99
|
+
executable (the fixture does), though the emission does not invoke it;
|
|
100
|
+
Packer runs the shell on the build VM. `parameters:` is refused at load (a
|
|
101
|
+
base-model rule). `config:` on the builder is carried but never read.
|
|
102
|
+
|
|
103
|
+
### `BashModItemModel`
|
|
104
|
+
|
|
105
|
+
Same file. Extends
|
|
106
|
+
[`ModItemModel`](../base/src/cs_image_system/base/models/moditem_type.py)
|
|
107
|
+
(`SubRootItem` -> `RootItem` -> `NameTyped`).
|
|
108
|
+
|
|
109
|
+
| Field | Type | Default | Meaning |
|
|
110
|
+
|---|---|---|---|
|
|
111
|
+
| `script` | `list[str]` | `[]` | Inline command lines, run in order in one `provisioner "shell"` (`inline = [...]`). Literal shell; no templating. Blank entries are dropped. |
|
|
112
|
+
| `scripts` | `list[str]` | `[]` | Script files, relative to the configuration root (the process's working directory, which the configuration load sets to the root), copied beside the Packer root and run as `scripts = [...]`. Blank entries are dropped. |
|
|
113
|
+
| `ensure` | `dict[str, Any]` | `{}` | The declarative form. Allowed keys: `packages` (list of names), `files` (list of `{path, content, mode}`; `mode` defaults to `0644`), `services` (list of unit names, enabled and started), `commands` (list of `{run, unless}`; `run` executes only when `unless` fails). Every entry is validated at load by `validate_ensure` (stage 63 item 16): a wrong shape, an unknown key, a bad mode or an all-empty mapping raises `ValueError` naming the item and the entry. |
|
|
114
|
+
|
|
115
|
+
Validation at load: at least one of `script`, `scripts`, `ensure` must be
|
|
116
|
+
non-empty, otherwise `ValueError` ("must supply 'script' (inline lines),
|
|
117
|
+
'scripts' (script files) and/or 'ensure' (declarative, idempotent)").
|
|
118
|
+
|
|
119
|
+
Derived behaviour:
|
|
120
|
+
|
|
121
|
+
- `idempotent`: `declared` when only `ensure` is given, `unknown`
|
|
122
|
+
otherwise. This is what the lineage record and the local bundle's
|
|
123
|
+
`MANIFEST.yaml` report.
|
|
124
|
+
- `ensure_lines()`: guarded shell for the declarative form, so re-running
|
|
125
|
+
is a no-op. Packages: `rpm -q` or `dpkg -s` first; only when missing,
|
|
126
|
+
install with the FIRST of `dnf`, `yum`, `apt-get` that exists on the
|
|
127
|
+
host, and stop the bake if that one fails (the others are never tried).
|
|
128
|
+
With none of the three, the line says so and fails. Files: content is base64-embedded,
|
|
129
|
+
decoded to a temp file, and `install -m <mode>` runs only when `cmp`
|
|
130
|
+
says the target differs. Services: `systemctl enable` / `start` only when
|
|
131
|
+
not already enabled / active. Commands: `( <unless> ) || { <run>; }`, or
|
|
132
|
+
the bare `run` when there is no `unless`.
|
|
133
|
+
- `remap_self_with_copied_assets(copied)`: replaces each `scripts` entry
|
|
134
|
+
with its copied path when present in the mapping.
|
|
135
|
+
|
|
136
|
+
Inherited item fields:
|
|
137
|
+
|
|
138
|
+
| Base field | From | Default | Use in this plugin |
|
|
139
|
+
|---|---|---|---|
|
|
140
|
+
| `name` | `NameTyped` | required | Names the provisioner comment and the local bundle directory (`NN-<name>`). |
|
|
141
|
+
| `type` | `ModItemModel` | `default` | Foreign key to a `mod_builders:` entry (see above). |
|
|
142
|
+
| `description`, `aliases`, `tags` | `NameTyped` / `RootItem` | | Carried; not emitted. |
|
|
143
|
+
| `config` | `RootItem` | `{}` | Not read by this plugin. It takes part in the lineage content hash of the modification and nothing else. |
|
|
144
|
+
| `_model_id` | `ModItemModel` | set at load | The owning image's global id; `identified_model` resolves to the image. |
|
|
145
|
+
|
|
146
|
+
### A modification item with only `config:`
|
|
147
|
+
|
|
148
|
+
An item that declares `config:` and none of `script`, `scripts`, `ensure`
|
|
149
|
+
does not load: `BashModItemModel.__post_init__` raises the `ValueError`
|
|
150
|
+
above, so the configuration fails at load and nothing is generated. `config:`
|
|
151
|
+
alone is never a modification; nothing in this plugin reads it.
|
|
152
|
+
|
|
153
|
+
## The builder
|
|
154
|
+
|
|
155
|
+
`BashModBuilder`
|
|
156
|
+
([bash_builder.py](src/cs_image_system/bash_mod_plugin/bash_builder.py))
|
|
157
|
+
extends
|
|
158
|
+
[`ModBuilderBase`](../base/src/cs_image_system/base/basic/builder_base_mod.py).
|
|
159
|
+
A modification builder implements the three per-item hooks the image
|
|
160
|
+
builder calls while it writes a build file; it does not implement the
|
|
161
|
+
lifecycle-level hooks (`generate_items_before/during/after`,
|
|
162
|
+
`get_commands_to_run_*`, `pre/post_finalize_phase`), so it emits no files
|
|
163
|
+
of its own, runs no generation-time commands and defers nothing to a
|
|
164
|
+
`run-<lifecycle>.sh`.
|
|
165
|
+
|
|
166
|
+
For every modification of an instance image, the Packer image builder
|
|
167
|
+
calls, in order:
|
|
168
|
+
|
|
169
|
+
1. `copy_external_assets(block_dir, mod)`: copies each of the **item's**
|
|
170
|
+
`scripts` to `<block_dir>/<script path>` and returns
|
|
171
|
+
`{script: script path}`. The source is resolved against the process's
|
|
172
|
+
current working directory; the configuration load has already changed
|
|
173
|
+
directory to the configuration root (`--root-dir`, else
|
|
174
|
+
`working_directory` in `cfg/_config.yml`, else `.`), so in practice the
|
|
175
|
+
path is root-relative. A missing file raises `FileNotFoundError`; an
|
|
176
|
+
absolute path is not copied and is left in place.
|
|
177
|
+
2. `mod.remap_self_with_copied_assets(mapping)` on the item. Because the
|
|
178
|
+
mapping's value is the same relative path the copy was placed at, the
|
|
179
|
+
item's `scripts` entries keep their text; the emitted `scripts = [...]`
|
|
180
|
+
is relative to the Packer root, where the copy now lies.
|
|
181
|
+
3. `generate_items_before_modification`: returns an empty asset set.
|
|
182
|
+
4. `generate_items_during_modification(image, mod, image_builder, phase, build_path)`:
|
|
183
|
+
- `inline` = `ensure_lines()` followed by the `script` lines; `scripts`
|
|
184
|
+
= the script files. When both lists are empty it logs a warning and
|
|
185
|
+
emits nothing; since stage 63 item 16 an all-empty `ensure` is
|
|
186
|
+
refused at load, so an item can no longer reach this.
|
|
187
|
+
- Appends `# Modifications for <item name> of type <item type> (shell)`.
|
|
188
|
+
- Then, because Packer's shell provisioner takes either `scripts` or
|
|
189
|
+
`inline` but never both, one or two `provisioner "shell"` blocks:
|
|
190
|
+
first `scripts = ["..."]` when there are script files, then
|
|
191
|
+
`inline = [ "...", ]` when there are inline lines. Each block carries
|
|
192
|
+
`only = ["<source type>.<image>"]` and, when set on the builder model,
|
|
193
|
+
`execute_command`, `environment_vars` and `expect_disconnect = true`.
|
|
194
|
+
The `execute_command` written is the model's
|
|
195
|
+
`effective_execute_command()`: the declared `execute_command`, else,
|
|
196
|
+
with a `configuration_user`, `chmod +x {{ .Path }}; sudo su - <user>
|
|
197
|
+
-c '{{ .Vars }} {{ .Path }}'` (Packer's `{{ .Vars }}`, the
|
|
198
|
+
environment variables, travel inside the command because `su -`
|
|
199
|
+
starts a fresh login environment), else none (stage 63).
|
|
200
|
+
- Every string is rendered as an HCL literal: backslashes and double
|
|
201
|
+
quotes are escaped, and `${` / `%{` become `$${` / `%%{` so shell text
|
|
202
|
+
such as `dpkg-query -f='${Package}'` survives Packer's template parser.
|
|
203
|
+
5. `generate_items_after_modification`: returns an empty asset set.
|
|
204
|
+
|
|
205
|
+
The provisioners run on the build VM when the image builder's deferred
|
|
206
|
+
`packer build .` executes. Separately, the Packer plugin's local
|
|
207
|
+
modification bundle records the item: its directory
|
|
208
|
+
`csis-mods/<image>/NN-<name>/` receives a copy of each script file, an
|
|
209
|
+
`inline.sh` holding the item's guarded `ensure` lines followed by its
|
|
210
|
+
`script` lines (with `set -eu`; the same order the bake runs them), a
|
|
211
|
+
`run.sh` that runs `sh '<script>'` per script file and then
|
|
212
|
+
`sh ./inline.sh`, and a `MANIFEST.yaml` with `operation: bash` and the
|
|
213
|
+
item's `idempotent` value. `csis-mods rerun` on the image therefore
|
|
214
|
+
re-applies the declarative `ensure` form too (stage 63). Until 2026-09-25
|
|
215
|
+
the `ensure` lines were not in the bundle and a rerun replayed only the
|
|
216
|
+
script files and the `script` lines.
|
|
217
|
+
|
|
218
|
+
## Emission
|
|
219
|
+
|
|
220
|
+
Nothing under `generated/` belongs to this plugin alone; its output is
|
|
221
|
+
embedded in the Packer plugin's files. From the golden emission for the
|
|
222
|
+
`derivative-setup` item of `imgfile-basic-dask` in
|
|
223
|
+
[pckr-ebs-ans-image-generation-block-000-build.pkr.hcl](../../tests/fixtures/v2_golden/generated/instance-image/pckr-ebs-ans/image-generation/block-000/pckr-ebs-ans-image-generation-block-000-build.pkr.hcl):
|
|
224
|
+
|
|
225
|
+
```hcl
|
|
226
|
+
# Modifications for derivative-setup of type bash-remote (shell)
|
|
227
|
+
provisioner "shell" {
|
|
228
|
+
only = ["amazon-ebs.imgfile-basic-dask"]
|
|
229
|
+
scripts = ["mod_image.sh"]
|
|
230
|
+
}
|
|
231
|
+
provisioner "shell" {
|
|
232
|
+
only = ["amazon-ebs.imgfile-basic-dask"]
|
|
233
|
+
inline = [
|
|
234
|
+
"for p in git; do rpm -q \"$p\" >/dev/null 2>&1 || dpkg -s \"$p\" >/dev/null 2>&1 || if command -v dnf >/dev/null 2>&1; then ... fi || exit 1; done",
|
|
235
|
+
"printf '%s' 'dmVyc2lvbj0xLjAuMAo=' | base64 -d > /tmp/.csis-ensure && ( sudo cmp -s /tmp/.csis-ensure '/etc/derivative.conf' || sudo install -m 0644 /tmp/.csis-ensure '/etc/derivative.conf' ) && rm -f /tmp/.csis-ensure",
|
|
236
|
+
"( test -d /opt/derivative/data ) >/dev/null 2>&1 || { sudo mkdir -p /opt/derivative/data; }",
|
|
237
|
+
"echo 'derivative setup'",
|
|
238
|
+
"sudo mkdir -p /opt/derivative && echo 1.0.0 | sudo tee /opt/derivative/VERSION",
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The three `ensure` lines (package, file, command) come first, then the two
|
|
244
|
+
`script` lines. Files it causes to exist beside the Packer root
|
|
245
|
+
(`generated/instance-image/pckr-ebs-ans/image-generation/block-000/`):
|
|
246
|
+
|
|
247
|
+
| File | Origin |
|
|
248
|
+
|---|---|
|
|
249
|
+
| `mod_image.sh` | The item's script file, copied by `copy_external_assets`. |
|
|
250
|
+
| [csis-mods/imgfile-basic-dask/02-derivative-setup/](../../tests/fixtures/v2_golden/generated/instance-image/pckr-ebs-ans/image-generation/block-000/csis-mods/imgfile-basic-dask/02-derivative-setup/MANIFEST.yaml) `MANIFEST.yaml`, `run.sh`, `inline.sh`, `mod_image.sh` | The local bundle entry the Packer plugin writes for this item (`operation: bash`, `idempotent: unknown`, `files: [mod_image.sh, inline.sh]`). |
|
|
251
|
+
|
|
252
|
+
The GCE builder's root
|
|
253
|
+
([pckr-gce-ans/.../block-000](../../tests/fixtures/v2_golden/generated/instance-image/pckr-gce-ans/image-generation/block-000/pckr-gce-ans-image-generation-block-000-build.pkr.hcl))
|
|
254
|
+
carries the same two blocks with `only = ["googlecompute.imgfile-basic-dask"]`.
|
|
255
|
+
|
|
256
|
+
## Example configuration
|
|
257
|
+
|
|
258
|
+
The builder, from
|
|
259
|
+
[tests/fixtures/config/cfg/mod-builders.yml](../../tests/fixtures/config/cfg/mod-builders.yml):
|
|
260
|
+
|
|
261
|
+
```yaml
|
|
262
|
+
mod_builders:
|
|
263
|
+
- name: bash-remote
|
|
264
|
+
aliases:
|
|
265
|
+
- bash
|
|
266
|
+
is_default: false
|
|
267
|
+
type: bash-remote
|
|
268
|
+
executable: bash
|
|
269
|
+
config:
|
|
270
|
+
some_unknown_mod_builder_setting: "{{ ENV.USER }} and some_value"
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The item that uses it, from
|
|
274
|
+
[tests/fixtures/config/images/image1.yaml](../../tests/fixtures/config/images/image1.yaml)
|
|
275
|
+
(the enclosing image is `imgfile-basic-dask`; comments removed):
|
|
276
|
+
|
|
277
|
+
```yaml
|
|
278
|
+
modifications:
|
|
279
|
+
- name: derivative-setup
|
|
280
|
+
type: bash-remote
|
|
281
|
+
script:
|
|
282
|
+
- "echo 'derivative setup'"
|
|
283
|
+
- "sudo mkdir -p /opt/derivative && echo 1.0.0 | sudo tee /opt/derivative/VERSION"
|
|
284
|
+
scripts:
|
|
285
|
+
- mod_image.sh
|
|
286
|
+
ensure:
|
|
287
|
+
packages: [git]
|
|
288
|
+
files:
|
|
289
|
+
- path: /etc/derivative.conf
|
|
290
|
+
content: "version=1.0.0\n"
|
|
291
|
+
mode: "0644"
|
|
292
|
+
commands:
|
|
293
|
+
- run: "sudo mkdir -p /opt/derivative/data"
|
|
294
|
+
unless: "test -d /opt/derivative/data"
|
|
295
|
+
config:
|
|
296
|
+
derivative_version: "1.0.0"
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The script path is relative to the configuration root
|
|
300
|
+
([tests/fixtures/config/mod_image.sh](../../tests/fixtures/config/mod_image.sh)).
|
|
301
|
+
Note the quoted `mode: "0644"`. An unquoted `0644` also works: YAML reads
|
|
302
|
+
it as the integer 420, and the loader renders an integer back in octal
|
|
303
|
+
(`install -m 0644`). What does NOT work is an unquoted `644` without the
|
|
304
|
+
leading zero: YAML reads the decimal number 644, which is no mode a file
|
|
305
|
+
should have, and the loader refuses any integer above `0777` and asks for
|
|
306
|
+
quotes. Quoting is the habit that never needs the rule.
|
|
307
|
+
|
|
308
|
+
## Prerequisites and integration
|
|
309
|
+
|
|
310
|
+
Nothing beyond the core on the operator's machine, with one declaration the
|
|
311
|
+
core demands and two facts about where the shell actually runs.
|
|
312
|
+
|
|
313
|
+
**The `bash` executable entry.** The builder's `executable:` must name an
|
|
314
|
+
entry in `cfg/executables.yml`
|
|
315
|
+
([the fixture's](../../tests/fixtures/config/cfg/executables.yml) is
|
|
316
|
+
`name: bash`, `binary: /usr/local/bin/bash`, no `version`), because
|
|
317
|
+
`validate` checks every builder's `executable` against that list and every
|
|
318
|
+
run checks every declared entry (stage 48: the binary must exist as an
|
|
319
|
+
absolute path or a name on `PATH`, `BashVersionChecker` must parse a version
|
|
320
|
+
from `<binary> --version`, and when the entry declares `version:` the parsed
|
|
321
|
+
version must satisfy it). That is the whole extent of the plugin's use of a
|
|
322
|
+
local bash: it is looked at, never run. The fixture declares no floor;
|
|
323
|
+
`GNU bash, version 3.2.57` (macOS `/bin/bash`) and `5.3.15` both parse.
|
|
324
|
+
|
|
325
|
+
**The build VM.** The provisioners execute on the machine Packer boots for
|
|
326
|
+
the bake, reached the way the image builder's source reaches it (ssh over
|
|
327
|
+
Session Manager on AWS, IAP on GCE; see [packer-plugin](../packer-plugin)).
|
|
328
|
+
This plugin holds no credentials and opens no connection of its own. On that
|
|
329
|
+
VM it assumes:
|
|
330
|
+
|
|
331
|
+
- a POSIX `sh` for the inline lines (Packer's shell provisioner runs them
|
|
332
|
+
through its `inline_shebang`, `/bin/sh -e` unless `execute_command` or the
|
|
333
|
+
Packer defaults are changed) and whatever interpreter each script file's
|
|
334
|
+
own shebang names;
|
|
335
|
+
- with `configuration_user` set, that user must exist on the VM, and the
|
|
336
|
+
bake's ssh user must be able to `sudo su - <user>` without a password;
|
|
337
|
+
- passwordless `sudo` for the bake's ssh user: every `ensure` line that
|
|
338
|
+
changes the system runs it through `sudo`;
|
|
339
|
+
- for `ensure.packages`: `rpm` or `dpkg` to query, and `dnf`, `yum` or
|
|
340
|
+
`apt-get` to install, plus network reach to the image's package
|
|
341
|
+
repositories when something is missing;
|
|
342
|
+
- for `ensure.files`: `base64`, `cmp` (diffutils) and `install`
|
|
343
|
+
(coreutils);
|
|
344
|
+
- for `ensure.services`: `systemctl`.
|
|
345
|
+
|
|
346
|
+
**The modification tests.** `just test-mods` applies the same item twice in
|
|
347
|
+
a throwaway container on the operator's machine
|
|
348
|
+
([mod_tests.py](../base/src/cs_image_system/base/mod_tests.py)); it needs
|
|
349
|
+
`docker` on `PATH` and a daemon that answers `docker info`, no cloud
|
|
350
|
+
session. The container gets a `sudo` shim when it has none, so the `sudo`
|
|
351
|
+
in `ensure` lines works there too.
|
|
352
|
+
|
|
353
|
+
**How the plugin finds things.** The builder by the item's `type:` (the
|
|
354
|
+
builder's name); the item class by the builder's `type: bash-remote`; the
|
|
355
|
+
`bash` entry by the builder's `executable:`; script files by path relative
|
|
356
|
+
to the configuration root (the working directory the load selects from
|
|
357
|
+
`--root-dir`, else `cfg/_config.yml`'s `working_directory`, else `.`); the
|
|
358
|
+
build VM through the image builder that hosts the emission. No environment
|
|
359
|
+
variable, profile or file of its own.
|
|
360
|
+
|
|
361
|
+
## Configuration reference
|
|
362
|
+
|
|
363
|
+
### The builder (`cfg/mod-builders.yml`, `type: bash-remote`)
|
|
364
|
+
|
|
365
|
+
Every key the model accepts. An unknown key is refused at load
|
|
366
|
+
(`extra="forbid"` in the shared model config), and `parameters:` is refused
|
|
367
|
+
by name (retired in stage 26).
|
|
368
|
+
|
|
369
|
+
| Field | Type | Default | Meaning |
|
|
370
|
+
|---|---|---|---|
|
|
371
|
+
| `name` | str | required | The builder's name; what an item's `type:` must say. Characters `/` and `\` are refused; `default` is refused as a name. |
|
|
372
|
+
| `type` | str | required | `bash-remote`. Selects this plugin's model and builder. |
|
|
373
|
+
| `description` | str or null | `null` | Free text. Accepted, not read. |
|
|
374
|
+
| `aliases` | list[str] | `[]` | Extra names an item's `type:` may use; the loader rewrites them to the builder's `name`. |
|
|
375
|
+
| `executable` | str or null | `null` | Must name an `executables:` entry (the fixture: `bash`), or `validate` fails. Never invoked by this plugin. |
|
|
376
|
+
| `is_default` | bool | `false` | Makes this the builder an item with `type: default` (or no `type`) resolves to. |
|
|
377
|
+
| `config` | mapping | `{}` | Accepted, not read. |
|
|
378
|
+
| `gitignore` | list[str] | `[]` | Accepted by the base model; nothing in this plugin reads it. |
|
|
379
|
+
| `tags` | mapping | `{}` | Accepted, not read. |
|
|
380
|
+
| `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`. |
|
|
381
|
+
| `configuration_user` | str or null | `null` | Read since stage 63 (accepted and ignored until 2026-09-25): 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`). |
|
|
382
|
+
| `environment_vars` | list[str] | `[]` | Emitted as `environment_vars = [...]` on every shell block. |
|
|
383
|
+
| `expect_disconnect` | bool | `false` | When true, `expect_disconnect = true` on every shell block. |
|
|
384
|
+
| `extra_arguments` | | | Refused at load as an unknown key (removed in stage 63; it was accepted and never read). Use `execute_command`. |
|
|
385
|
+
|
|
386
|
+
### The item (an image's `modifications:` entry whose `type:` names this builder)
|
|
387
|
+
|
|
388
|
+
| Field | Type | Default | Meaning |
|
|
389
|
+
|---|---|---|---|
|
|
390
|
+
| `name` | str | required | Unique within the image. Names the comment line, the bundle directory `NN-<name>` and the lineage record. |
|
|
391
|
+
| `type` | str | `default` | The builder's name, an alias, or `default` (or omitted). Rewritten to the builder's name at load (stage 63 item 15). Omitted or `default` with no `is_default: true` mod builder: refused at load (stage 63). |
|
|
392
|
+
| `description` | str or null | `null` | Accepted, not read. |
|
|
393
|
+
| `aliases` | list[str] | `[]` | Accepted, not read. |
|
|
394
|
+
| `tags` | mapping | `{}` | Accepted, not read. |
|
|
395
|
+
| `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. |
|
|
396
|
+
| `script` | list[str] | `[]` | Inline lines, literal, in order, after the `ensure` lines. Blank and whitespace-only entries are dropped. |
|
|
397
|
+
| `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. |
|
|
398
|
+
| `ensure` | mapping | `{}` | Only the four keys below are allowed; any other key is refused at load. Every entry's values are validated at load since stage 63 item 16 (`validate_ensure`; see "What it tests and verifies"). |
|
|
399
|
+
|
|
400
|
+
The `ensure` keys:
|
|
401
|
+
|
|
402
|
+
| Key | Type | Default | Meaning |
|
|
403
|
+
|---|---|---|---|
|
|
404
|
+
| `packages` | list[str] | `[]` | One `for` line: each name is queried with `rpm -q` then `dpkg -s`; only when both fail, installed with `dnf -y`, else `yum -y`, else `apt-get install -y`, under `sudo`. |
|
|
405
|
+
| `files` | list of mappings | `[]` | One line per entry: `path` (required), `content` (default empty), `mode` (default `0644`, quote it). Content is base64-embedded in the build file, decoded to `/tmp/.csis-ensure` and installed with `sudo install -m <mode>` only when `sudo cmp -s` says the target differs. |
|
|
406
|
+
| `services` | list[str] | `[]` | Two lines per unit: `sudo systemctl enable` unless already enabled, `sudo systemctl start` unless already active. |
|
|
407
|
+
| `commands` | list of mappings | `[]` | One line per entry: `run` (required) and optional `unless`. With `unless`: `( <unless> ) >/dev/null 2>&1 \|\| { <run>; }`. Without: the bare `run`, every time. |
|
|
408
|
+
|
|
409
|
+
### Variations
|
|
410
|
+
|
|
411
|
+
- **`script` only** is one `provisioner "shell"` with `inline`. **`scripts`
|
|
412
|
+
only** is one block with `scripts`. **Both** are two blocks, the files
|
|
413
|
+
first, because Packer refuses both arguments in one provisioner.
|
|
414
|
+
- **`ensure` only** is recorded as `idempotent: declared` (lineage and the
|
|
415
|
+
bundle manifest); **`ensure` with `script` or `scripts`** is
|
|
416
|
+
`idempotent: unknown`, the same as free-form alone. `ensure` lines always
|
|
417
|
+
precede the `script` lines in the inline block.
|
|
418
|
+
- **`ensure` whose lists are all empty** (`ensure: {packages: []}`) is
|
|
419
|
+
refused at load (stage 63 item 16). Before 2026-09-25 it passed load,
|
|
420
|
+
counted as `declared`, and emitted nothing: the builder logged
|
|
421
|
+
`Bash modification <name> has no script lines or files; nothing emitted`
|
|
422
|
+
and the bake proceeded without a provisioner for it. The bundle directory
|
|
423
|
+
still exists, with a `run.sh` that runs nothing.
|
|
424
|
+
- **`commands` with `unless`** is guarded; **without `unless`** the command
|
|
425
|
+
runs on every bake and on every `csis-mods rerun`, so the item is
|
|
426
|
+
idempotent only if the command is.
|
|
427
|
+
- **A relative script path** is copied to the same relative path beside the
|
|
428
|
+
Packer root and emitted relative; **an absolute path** is emitted
|
|
429
|
+
absolute and Packer reads it from the operator's machine at build time.
|
|
430
|
+
- **`execute_command` set** puts that template on every block of every
|
|
431
|
+
item of the builder; **`configuration_user` set** instead puts
|
|
432
|
+
`chmod +x {{ .Path }}; sudo su - <user> -c '{{ .Vars }} {{ .Path }}'`
|
|
433
|
+
there, so the lines run as that user (stage 63); **both set** is refused
|
|
434
|
+
at load; **neither**, Packer's default applies and the lines run as the
|
|
435
|
+
bake user. The same for
|
|
436
|
+
`environment_vars` (emitted only when non-empty) and `expect_disconnect`
|
|
437
|
+
(emitted only when true).
|
|
438
|
+
- **`type: <builder name>`** reaches this plugin. **`type: default` or no
|
|
439
|
+
`type`** reaches whichever mod builder is `is_default: true`; in the
|
|
440
|
+
fixture that is `ansible-default`, so the item is loaded as an ANSIBLE
|
|
441
|
+
item and its `script`/`scripts`/`ensure` keys are refused as unknown.
|
|
442
|
+
With no `is_default: true` mod builder at all, the load is refused
|
|
443
|
+
(stage 63): `No default builder found in mod_builder list.` first, and
|
|
444
|
+
the orchestrator's `` modification '<name>' names no builder (`type`) ... ``
|
|
445
|
+
for an item that reaches it anyway. Until 2026-09-25 such an item
|
|
446
|
+
became an `AttributeError` at generation.
|
|
447
|
+
**`type: <alias>`** behaves exactly as the builder's name: the loader
|
|
448
|
+
rewrites it.
|
|
449
|
+
- **AWS versus GCE runtime**: the lines are identical; only the `only`
|
|
450
|
+
label differs (`amazon-ebs.<image>` versus `googlecompute.<image>`), and
|
|
451
|
+
the same blocks land in every image builder's root the image bakes on.
|
|
452
|
+
- **Dry run versus real run**: generation is the same, so the build file,
|
|
453
|
+
the copied scripts and the bundle appear under `generated/` either way.
|
|
454
|
+
A dry run never runs `packer build`, so nothing executes on a VM and no
|
|
455
|
+
lineage record is written. `--no-apply` generates and skips
|
|
456
|
+
finalization too.
|
|
457
|
+
- **Base image versus instance image**: a base image's `modifications:`
|
|
458
|
+
are ignored with a warning by the image builder; only instance images
|
|
459
|
+
reach this plugin.
|
|
460
|
+
- **Bake versus `test-mods`**: in the bake, the inline lines run under
|
|
461
|
+
Packer's `/bin/sh -e` on the VM; under `test-mods` the same lines (ensure
|
|
462
|
+
lines included) are written to an `inline.sh` with `set -eu` and run with
|
|
463
|
+
`sh` inside the container, after each script file, as root behind a
|
|
464
|
+
`sudo` shim. A line that passes in one and fails in the other usually
|
|
465
|
+
differs on `sudo`, on `$HOME`, or on a package repository the container
|
|
466
|
+
cannot reach.
|
|
467
|
+
|
|
468
|
+
## What it tests and verifies
|
|
469
|
+
|
|
470
|
+
**At load** (pydantic validators and `__post_init__`, before any command
|
|
471
|
+
does anything). Unknown keys on the builder or the item; `parameters:` on
|
|
472
|
+
the builder; `/` or `\` in a name or alias; `ensure` keys outside
|
|
473
|
+
`packages`, `files`, `services`, `commands`; every `ensure` entry's shape
|
|
474
|
+
(`packages`/`services` a list of non-empty names, `files` entries with a
|
|
475
|
+
`path` and only `path`/`content`/`mode`, `commands` entries with a `run`
|
|
476
|
+
and only `run`/`unless`, a mode of three or four octal digits, an
|
|
477
|
+
integer mode no higher than `0777`, and not every kind empty); an item
|
|
478
|
+
with none of `script`, `scripts`, `ensure`; an item `type:` that names no
|
|
479
|
+
builder; an item with no `type:` when no mod builder is the default; a
|
|
480
|
+
builder declaring both `configuration_user` and `execute_command`, or
|
|
481
|
+
the removed `extra_arguments` (stage 63). Each is a
|
|
482
|
+
`ValueError` (or `KeyError` for the type) that stops the load; the CLI
|
|
483
|
+
prints it and exits 1. Nothing loads, so nothing is generated.
|
|
484
|
+
|
|
485
|
+
**At `validate`** ([validate.py](../base/src/cs_image_system/base/commands/validate.py)).
|
|
486
|
+
The builder's `executable:` must name an `executables:` entry; the `bash`
|
|
487
|
+
entry's binary must exist, its version must parse and, when declared, meet
|
|
488
|
+
the requirement. Every failure is collected by name and printed; `validate`
|
|
489
|
+
exits 1 and `run` refuses before generation. One INFO line lists every
|
|
490
|
+
checked tool and the version found (`bash 5.3.15 (no requirement)` for the
|
|
491
|
+
fixture).
|
|
492
|
+
|
|
493
|
+
**At generation.** Two checks, both fatal to the run: a `scripts` entry
|
|
494
|
+
that is not a file (`FileNotFoundError`), and the assertion that the item's
|
|
495
|
+
`type` is a builder name. Neither is a validation error; the run summary
|
|
496
|
+
reports `ok: false` with an empty validation list and the traceback is in
|
|
497
|
+
the run log. The plugin does not validate the HCL it writes; the Packer
|
|
498
|
+
plugin's run-now commands (`packer fmt`, `init`, `validate`) do, and a
|
|
499
|
+
malformed block fails there before any bake. An empty emission is a WARNING
|
|
500
|
+
line, not a failure.
|
|
501
|
+
|
|
502
|
+
**At apply.** Nothing by this plugin. Packer runs the blocks in order; the
|
|
503
|
+
inline lines run under `-e`, so the first failing line aborts the
|
|
504
|
+
provisioner and the build, no image is produced and no lineage record is
|
|
505
|
+
written. The `ensure` lines are the only ones the plugin vouches for as
|
|
506
|
+
safe to re-run.
|
|
507
|
+
|
|
508
|
+
**After apply.** The Packer plugin's post-finalize hook records every
|
|
509
|
+
modification of the built image in `meta-state/lineage.yaml`
|
|
510
|
+
([lineage.py](../base/src/cs_image_system/base/lineage.py) `mod_records`):
|
|
511
|
+
`operation: bash`, `idempotent` (`declared` or `unknown`), and a
|
|
512
|
+
`content_hash` over the item's type, `config`, `script` lines, the CONTENT
|
|
513
|
+
of each script file and the `ensure` mapping. That hash feeds the image's
|
|
514
|
+
`input_fingerprint`, which is what makes an edited modification re-bake the
|
|
515
|
+
image on the next run. The on-image bundle under `/opt/csis/mods/` carries
|
|
516
|
+
the same record.
|
|
517
|
+
|
|
518
|
+
**In `test-mods`** ([mod_tests.py](../base/src/cs_image_system/base/mod_tests.py)).
|
|
519
|
+
The item is applied twice in a container of the image's OS family. Run one
|
|
520
|
+
must exit 0; run two must exit 0 and change no file outside the volatile
|
|
521
|
+
paths (`docker diff`). The verdict lands in the live configuration's
|
|
522
|
+
`meta-state/mod-tests.yaml`, keyed by the content hash, as
|
|
523
|
+
`status: pass|fail|skipped|unsupported` and `idempotent: true|false`; a
|
|
524
|
+
failure prints `FAIL <image>/<mod>: rc=...` on stderr and exits 1. A
|
|
525
|
+
`release` of a build whose recorded modification has `fail` or
|
|
526
|
+
`idempotent: false` is refused; a missing record is tolerated unless
|
|
527
|
+
`config.require_mod_tests` is true.
|
|
528
|
+
|
|
529
|
+
**In the state query.** Nothing. The plugin owns no provider resource, so
|
|
530
|
+
`state query` has nothing of its to diff.
|
|
531
|
+
|
|
532
|
+
## When it fails
|
|
533
|
+
|
|
534
|
+
Failures that have happened, in date order:
|
|
535
|
+
|
|
536
|
+
- **2026-08-27, three defects found in the pre-V2 builder**
|
|
537
|
+
([LEDGER](../../docs/history/LEDGER.md), "Finding 2026-08-27"). Every
|
|
538
|
+
item resolved to the default mod builder (the lookup used the field name
|
|
539
|
+
`modifications`, not the item's `type`), so a bash item was recorded as
|
|
540
|
+
`ansible-default` with no content; the bash builder wrote raw lines and an
|
|
541
|
+
`exit 0` straight into the build file, so any real bash item failed
|
|
542
|
+
`packer validate`; and `scripts:` was not a field, so files were dropped.
|
|
543
|
+
Fixed at gate 8; guarded by
|
|
544
|
+
[test_v2_gate8_modifications.py](../../tests/test_v2_gate8_modifications.py)
|
|
545
|
+
and [tests/test_bash_provisioner.py](tests/test_bash_provisioner.py). If
|
|
546
|
+
a comment line ever appears outside a `provisioner "shell" {` block, or
|
|
547
|
+
`exit 0` appears in a build file, this regressed.
|
|
548
|
+
- **2026-09-10, the first `full-test`'s mod-test leg could not start a
|
|
549
|
+
container** for an EL 10 image: Docker Hub's `rockylinux` library stops at
|
|
550
|
+
9. The stand-in for `rhel` 10 and above is `almalinux:<version>` since
|
|
551
|
+
(`test_image_for` in [mod_tests.py](../base/src/cs_image_system/base/mod_tests.py)).
|
|
552
|
+
Symptom then: `docker run` failed to pull and the bash item's test never
|
|
553
|
+
ran. If `test-mods` reports `unsupported` with
|
|
554
|
+
`no local test image for the root OS family`, the family/version has no
|
|
555
|
+
stand-in; set `local_test_image` on the OS builder.
|
|
556
|
+
- **Found live, undated, in the sibling shell emitter for image tests**
|
|
557
|
+
([v2_provisioners.py](../packer-plugin/src/cs_image_system/packer_plugin/v2_provisioners.py)):
|
|
558
|
+
an `rpm --qf '%{NAME}'` line broke `packer fmt`, because `%{` and `${`
|
|
559
|
+
open Packer template directives. This plugin's `_hcl_string` escapes both
|
|
560
|
+
the same way. If `packer fmt` or `validate` ever reports
|
|
561
|
+
`Invalid template directive` or `Invalid template interpolation` on a
|
|
562
|
+
line you wrote, the escape regressed; the line in `generated/` should
|
|
563
|
+
read `%%{` / `$${`.
|
|
564
|
+
- **2026-09-20, an assertion that could not fail** (image `tests:`, not this
|
|
565
|
+
plugin, but the same shell and the same trap;
|
|
566
|
+
[OPERATIONS.md](../../docs/OPERATIONS.md), "What a green bake proves").
|
|
567
|
+
Under `sh -e`, a command inside an AND-OR list other than the last does
|
|
568
|
+
not abort the script, so a `script:` line written `check || { echo no; }`
|
|
569
|
+
never fails the build however false `check` is. Two bakes went green
|
|
570
|
+
asserting a package that did not exist. If a `script` line must be able
|
|
571
|
+
to stop the bake, end it in something that exits nonzero (`|| exit 1`),
|
|
572
|
+
not in a braced group that returns 0. The plugin's own `commands` form,
|
|
573
|
+
`( unless ) || { run; }`, is deliberate: the group is the LAST element,
|
|
574
|
+
so a failing `run` does abort.
|
|
575
|
+
- **2026-09-23, an item declared with the builder's alias failed at
|
|
576
|
+
generation** (found while writing this README, on a copy of the fixture
|
|
577
|
+
with `type: bash` on `derivative-setup`). Load and `validate` passed; the
|
|
578
|
+
run log then showed
|
|
579
|
+
`AssertionError: No mod builder bash found in context for imgfile-basic-dask`
|
|
580
|
+
and the summary was `ok: false` with no validation errors. Fixed by
|
|
581
|
+
stage 63 item 15 (2026-09-25): the loader rewrites the alias to the
|
|
582
|
+
builder's name, and `tests/test_v2_defects_loading.py` generates that
|
|
583
|
+
same fixture copy.
|
|
584
|
+
|
|
585
|
+
Failures the code raises that have not been seen outside tests:
|
|
586
|
+
|
|
587
|
+
| What you see | Where | Meaning and what to do |
|
|
588
|
+
|---|---|---|
|
|
589
|
+
| `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. |
|
|
590
|
+
| `Bash modification '<name>': unknown ensure keys ['users']` | load; CLI exit 1 | Only `packages`, `files`, `services`, `commands` exist. Move anything else to `commands`. |
|
|
591
|
+
| 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. |
|
|
592
|
+
| ``<builder>: `parameters` was retired (stage 26) ...`` | load; CLI exit 1 | Delete `parameters:` from the builder. |
|
|
593
|
+
| `KeyError: Could not resolve builder '<type>' referenced in Image(...)` | load; CLI exit 1 | The item's `type:` names no `mod_builders:` entry. |
|
|
594
|
+
| `` mod builder '<name>': `configuration_user` and `execute_command` both say how the script runs; declare one `` | load; CLI exit 1 | Stage 63. `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`. |
|
|
595
|
+
| A pydantic unknown-key error naming `extra_arguments` on a `bash-remote` builder | load; CLI exit 1 | Stage 63 removed the field (it was never read). Delete it; change the invocation with `execute_command`. |
|
|
596
|
+
| `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 `` (stage 63; it used to be an `AttributeError` at generation). |
|
|
597
|
+
| `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:`. |
|
|
598
|
+
| `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:`. |
|
|
599
|
+
| ``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. |
|
|
600
|
+
| `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. |
|
|
601
|
+
| `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`. |
|
|
602
|
+
| `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`. |
|
|
603
|
+
| `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`. Before stage 63 this was a `KeyError` at generation. |
|
|
604
|
+
| `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; the old code iterated its letters. Write a list (`packages: [git]`). |
|
|
605
|
+
| `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"`. |
|
|
606
|
+
| `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. |
|
|
607
|
+
| `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. |
|
|
608
|
+
| `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). Before stage 63 the line fell through to `apt-get` and showed `apt-get: command not found` last on an EL host. |
|
|
609
|
+
| `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. |
|
|
610
|
+
| `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. |
|
|
611
|
+
| `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). |
|
|
612
|
+
| `Build 'amazon-ebs.<image>' errored ...` and no new build in `meta-state/lineage.yaml` | apply (`run-instance-image.sh`, `set -euo pipefail`) | A line exited nonzero under `-e`. Packer's log names the provisioner by its position; match it to the `# Modifications for <name>` comment in the build file under `generated/`. Nothing is recorded for a failed bake; fix and re-run. |
|
|
613
|
+
| `FAIL <image>/<mod>: rc=1/None idempotent=None <tail of output>` | `test-mods`; exit 1 | Run one failed in the container. The output tail is the last 2000 characters; `meta-state/mod-tests.yaml` keeps it under the content hash. |
|
|
614
|
+
| `FAIL <image>/<mod>: rc=0/0 idempotent=False ['A /opt/x', ...]` | `test-mods`; exit 1 | Run two changed files. A `script` line that appends, or a `commands` entry without `unless`. Make it `ensure`, or add `unless`. |
|
|
615
|
+
| `mod tests: N tested, M skipped` with `docker is not available` in the JSON | `test-mods`; exit 1 only with `--strict` | Start docker. `just full-test` runs this leg with `--strict`. |
|
|
616
|
+
| `modification '<name>' of build '<id>' failed its local mod test (status=fail, idempotent=...)` | `release`; exit 1 | Fix the modification, re-bake, re-test. A release never overrides a failed record. |
|
|
617
|
+
| `modification '<name>' of build '<id>' has no local mod-test result (config.require_mod_tests)` | `release`; exit 1 | Run `just test-mods` on the live tree and commit `meta-state/mod-tests.yaml`. |
|
|
618
|
+
| `local mods: <src> for <name> not found; the bundle will lack it` | generation log (Packer plugin) | The bundle could not find a script file under the block dir, the working path or as given; the bake still emits the provisioner. Usually the same cause as the `FileNotFoundError` above, seen when the file was removed between copy and bundle. |
|
|
619
|
+
|
|
620
|
+
## Related
|
|
621
|
+
|
|
622
|
+
- Manuals: [CONFIGURATION.md](../../docs/CONFIGURATION.md) sections 3 and
|
|
623
|
+
7 (executables, mod builders, the modification item),
|
|
624
|
+
[OPERATIONS.md](../../docs/OPERATIONS.md) (the modification tests, what a
|
|
625
|
+
green bake proves), [DESIGN.md](../../docs/DESIGN.md) M2 (the bash mod
|
|
626
|
+
contract).
|
|
627
|
+
- Base models extended:
|
|
628
|
+
[mod_builder.py](../base/src/cs_image_system/base/models/mod_builder.py),
|
|
629
|
+
[moditem_type.py](../base/src/cs_image_system/base/models/moditem_type.py),
|
|
630
|
+
[root_item.py](../base/src/cs_image_system/base/models/root_item.py),
|
|
631
|
+
[builder_model.py](../base/src/cs_image_system/base/models/builder_model.py).
|
|
632
|
+
- Base builder class:
|
|
633
|
+
[builder_base_mod.py](../base/src/cs_image_system/base/basic/builder_base_mod.py);
|
|
634
|
+
the label helper `build_target_label_of` in
|
|
635
|
+
[builder_base_image.py](../base/src/cs_image_system/base/basic/builder_base_image.py);
|
|
636
|
+
the version checker base in
|
|
637
|
+
[abstract_version_checker.py](../base/src/cs_image_system/base/basic/abstract_version_checker.py).
|
|
638
|
+
- The image builder that calls it and hosts its output:
|
|
639
|
+
[packer-plugin](../packer-plugin)
|
|
640
|
+
([packer_ebs_builder.py](../packer-plugin/src/cs_image_system/packer_plugin/packer_ebs_builder.py),
|
|
641
|
+
[v2_local_mods.py](../packer-plugin/src/cs_image_system/packer_plugin/v2_local_mods.py)).
|
|
642
|
+
- Lineage records for modifications:
|
|
643
|
+
[lineage.py](../base/src/cs_image_system/base/lineage.py) (`mod_records`);
|
|
644
|
+
the release gate in [release.py](../base/src/cs_image_system/base/release.py).
|
|
645
|
+
- Tests: [tests/test_bash_provisioner.py](tests/test_bash_provisioner.py)
|
|
646
|
+
(two blocks for files plus inline, builder options, script copying);
|
|
647
|
+
[test_v2_gate8_modifications.py](../../tests/test_v2_gate8_modifications.py)
|
|
648
|
+
(both kinds emitted, unknown type refused, the load rule);
|
|
649
|
+
[test_v2_explore_local_mods.py](../../tests/test_v2_explore_local_mods.py)
|
|
650
|
+
(`ensure` guards, idempotence recorded, the bundle);
|
|
651
|
+
[test_v2_explore_mod_tests.py](../../tests/test_v2_explore_mod_tests.py)
|
|
652
|
+
(the container tests on the fixture's bash item);
|
|
653
|
+
[test_v2_hygiene_three.py](../../tests/test_v2_hygiene_three.py)
|
|
654
|
+
(`BashVersionChecker` found by name, versions judged).
|
|
655
|
+
- No terraform modules and no hashicorp-utils usage: the plugin writes
|
|
656
|
+
Packer provisioner lines directly.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
cs_image_system/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
cs_image_system/bash_mod_plugin/__init__.py,sha256=GAFRNaGWQxXquHOlCzxan4LkPZ9_NsiyswknxdPcX0c,122
|
|
3
|
+
cs_image_system/bash_mod_plugin/bash_builder.py,sha256=RUi1S-BMnkAOksXOXwx4Keq0rozxYgGgsJqVFMQgVQU,6245
|
|
4
|
+
cs_image_system/bash_mod_plugin/bash_models.py,sha256=9Pi3NKp7bB1BNecRpKFsGkZUJwzy2KoyMtw7gNdqNGo,11782
|
|
5
|
+
cs_image_system/bash_mod_plugin/main.py,sha256=1pWG7kr7w7ay2Dqzom58U8oRcUXFuf96iLmqVYfnLFA,1101
|
|
6
|
+
cs_image_system_bash_mod_plugin-0.1.1.dist-info/METADATA,sha256=cXnXNyddsxdXKlbP1koR434gjJhSlJDch39lHnR-u8U,44117
|
|
7
|
+
cs_image_system_bash_mod_plugin-0.1.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
8
|
+
cs_image_system_bash_mod_plugin-0.1.1.dist-info/entry_points.txt,sha256=yZSr8VNr4qw_sn9RjeR5U2xsuBfmFJok072qjsGx8xQ,101
|
|
9
|
+
cs_image_system_bash_mod_plugin-0.1.1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
10
|
+
cs_image_system_bash_mod_plugin-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|