cs-image-system-aws-runtime-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/aws_runtime/__init__.py +3 -0
- cs_image_system/aws_runtime/aws_packer_source.py +180 -0
- cs_image_system/aws_runtime/aws_provider_specific_image.py +43 -0
- cs_image_system/aws_runtime/aws_runtime_builders.py +518 -0
- cs_image_system/aws_runtime/aws_runtime_models.py +177 -0
- cs_image_system/aws_runtime/aws_utils.py +476 -0
- cs_image_system/aws_runtime/bootstrap.py +367 -0
- cs_image_system/aws_runtime/main.py +28 -0
- cs_image_system/py.typed +0 -0
- cs_image_system_aws_runtime_plugin-0.1.1.dist-info/METADATA +1146 -0
- cs_image_system_aws_runtime_plugin-0.1.1.dist-info/RECORD +14 -0
- cs_image_system_aws_runtime_plugin-0.1.1.dist-info/WHEEL +4 -0
- cs_image_system_aws_runtime_plugin-0.1.1.dist-info/entry_points.txt +5 -0
- cs_image_system_aws_runtime_plugin-0.1.1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""The ``amazon-ebs`` packer source for an image baked on an AWS runtime.
|
|
6
|
+
|
|
7
|
+
Moved here from the packer plugin (EXPLORE GCP increment 1): the packer
|
|
8
|
+
builder is provider-neutral and asks the runtime plugin for the source
|
|
9
|
+
block; this module is AWS's answer. Shape unchanged: a ``data "amazon-ami"``
|
|
10
|
+
lookup for the bake's source image (exact id when the parent is pinned,
|
|
11
|
+
N5) and a ``source "amazon-ebs"`` block carrying the merged tags.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import hcl2
|
|
19
|
+
from hcl2 import Builder
|
|
20
|
+
|
|
21
|
+
from cs_image_system.base.constants import OOPS_DEFAULTS
|
|
22
|
+
from cs_image_system.hashicorp_utils.hashicorp import FO
|
|
23
|
+
|
|
24
|
+
from .aws_runtime_models import AwsCloudBuilderModel, AwsCloudNetworkingModel
|
|
25
|
+
|
|
26
|
+
log = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
AMAZON_EBS = "amazon-ebs"
|
|
29
|
+
|
|
30
|
+
# Installs and starts the SSM agent when the vendor image lacks it (RHEL and
|
|
31
|
+
# Debian vendor AMIs do); Amazon Linux/Ubuntu already run it. Idempotent.
|
|
32
|
+
_SSM_BOOTSTRAP = """#!/bin/bash
|
|
33
|
+
set -x
|
|
34
|
+
dl() { if command -v curl >/dev/null 2>&1; then curl -fsSL "$1" -o "$2"; else wget -qO "$2" "$1"; fi; }
|
|
35
|
+
if ! command -v amazon-ssm-agent >/dev/null 2>&1 && [ ! -x /snap/bin/amazon-ssm-agent ]; then
|
|
36
|
+
R=us-east-2
|
|
37
|
+
if command -v curl >/dev/null 2>&1; then R=$(curl -s http://169.254.169.254/latest/meta-data/placement/region || echo us-east-2);
|
|
38
|
+
elif command -v wget >/dev/null 2>&1; then R=$(wget -qO- http://169.254.169.254/latest/meta-data/placement/region || echo us-east-2); fi
|
|
39
|
+
if command -v dnf >/dev/null 2>&1; then dnf install -y "https://s3.$R.amazonaws.com/amazon-ssm-$R/latest/linux_amd64/amazon-ssm-agent.rpm";
|
|
40
|
+
elif command -v yum >/dev/null 2>&1; then yum install -y "https://s3.$R.amazonaws.com/amazon-ssm-$R/latest/linux_amd64/amazon-ssm-agent.rpm";
|
|
41
|
+
elif command -v dpkg >/dev/null 2>&1; then
|
|
42
|
+
command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 || { apt-get -o DPkg::Lock::Timeout=600 update -qq; apt-get -o DPkg::Lock::Timeout=600 install -y wget; }
|
|
43
|
+
dl "https://s3.$R.amazonaws.com/amazon-ssm-$R/latest/debian_amd64/amazon-ssm-agent.deb" /tmp/ssm.deb && dpkg -i /tmp/ssm.deb; fi
|
|
44
|
+
fi
|
|
45
|
+
systemctl enable --now amazon-ssm-agent 2>/dev/null || true
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _root_device(image: Any, psi: Any) -> str:
|
|
50
|
+
"""The bake source's root device name: from the resolved vendor query
|
|
51
|
+
when available, else by the chain root's family."""
|
|
52
|
+
raw = getattr(psi, "raw_query_result", None)
|
|
53
|
+
if isinstance(raw, dict) and raw.get("RootDeviceName"):
|
|
54
|
+
return str(raw["RootDeviceName"])
|
|
55
|
+
fam = _chain_root_family(image)
|
|
56
|
+
return {"debian": "/dev/xvda", "amazon": "/dev/xvda"}.get(fam, "/dev/sda1")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _chain_root_family(image: Any) -> str:
|
|
60
|
+
try:
|
|
61
|
+
from cs_image_system.base.capabilities import image_chain
|
|
62
|
+
from cs_image_system.base.global_context import GlobalTypeContext
|
|
63
|
+
ctx = GlobalTypeContext()
|
|
64
|
+
name = image.get_name()
|
|
65
|
+
root = name if name in ctx.os_builders else image_chain(ctx, image)[1]
|
|
66
|
+
return (ctx.os_builders[root].get_family() or "").lower() if root in ctx.os_builders else ""
|
|
67
|
+
except Exception:
|
|
68
|
+
return ""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _bake_disk(image: Any, runtime: str) -> Any:
|
|
72
|
+
from cs_image_system.base.global_context import GlobalTypeContext
|
|
73
|
+
from cs_image_system.base.lineage import bake_disk_size
|
|
74
|
+
size = bake_disk_size(GlobalTypeContext(), image, runtime)
|
|
75
|
+
return size if size is not None else image.primary_disk_size
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _machine_type(subconfig: Any, model: AwsCloudBuilderModel) -> str:
|
|
79
|
+
for v in (getattr(subconfig, "machine_type", None),
|
|
80
|
+
getattr(subconfig, "default_machine_type", None)):
|
|
81
|
+
if v and v not in OOPS_DEFAULTS:
|
|
82
|
+
return str(v)
|
|
83
|
+
return str(model.get_default_machine_type())
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def amazon_ebs_source(model: AwsCloudBuilderModel, image: Any, *, runtime: str, source_type: str,
|
|
87
|
+
subconfig: Any, self_subconfig: Any, psi: Any, tags: dict[str, str],
|
|
88
|
+
final_name: str, pinned: str | None) -> list[str]:
|
|
89
|
+
networking: AwsCloudNetworkingModel = model.networking # type: ignore[assignment]
|
|
90
|
+
query_assets = psi.get_query_assets()
|
|
91
|
+
assert query_assets is not None, (
|
|
92
|
+
f"Provider-specific image for image {image.name} on runtime {runtime} "
|
|
93
|
+
"must have query assets to convert to source configuration.")
|
|
94
|
+
if pinned:
|
|
95
|
+
query_assets = {"filters": {"image-id": f'"{pinned}"'}, "owners": ['"self"']}
|
|
96
|
+
# Credentials for packer's AWS calls: the same named profile the tofu
|
|
97
|
+
# roots put in their provider blocks (found live: without it the
|
|
98
|
+
# amazon-ami datasource fails with "No valid credential sources found").
|
|
99
|
+
# stage 17: credentials is a declared object; get_credentials() is the
|
|
100
|
+
# one conversion point to the mapping every consumer wants.
|
|
101
|
+
creds = model.get_credentials() if hasattr(model, "get_credentials") else {}
|
|
102
|
+
profile = creds.get("profile_name")
|
|
103
|
+
if profile:
|
|
104
|
+
query_assets = {**query_assets, "profile": f'"{profile}"'}
|
|
105
|
+
|
|
106
|
+
runtime_data_id = f"{image.name}_{runtime}"
|
|
107
|
+
c: dict[str, Any] = {
|
|
108
|
+
"source_ami": f"data.amazon-ami.{runtime_data_id}.id",
|
|
109
|
+
"ami_name": final_name,
|
|
110
|
+
# bake size: the subconfig's machine_type, else its default_machine_type
|
|
111
|
+
# (what os-builder runtime YAML carries), else the runtime default
|
|
112
|
+
"instance_type": _machine_type(subconfig, model),
|
|
113
|
+
"region": model.region,
|
|
114
|
+
"vpc_id": networking.network,
|
|
115
|
+
"subnet_id": networking.default_subnet_id,
|
|
116
|
+
"tags": {k: f'"{v}"' for k, v in tags.items()},
|
|
117
|
+
"associate_public_ip_address": True,
|
|
118
|
+
}
|
|
119
|
+
if profile:
|
|
120
|
+
c["profile"] = profile
|
|
121
|
+
# stage 63: the runtime's ena_support/sriov_support reach the source when
|
|
122
|
+
# declared (they were accepted and read by nothing); packer's defaults
|
|
123
|
+
# apply when they are not
|
|
124
|
+
for flag in ("ena_support", "sriov_support"):
|
|
125
|
+
value = getattr(model, flag, None)
|
|
126
|
+
if value is not None:
|
|
127
|
+
c[flag] = bool(value)
|
|
128
|
+
# Debug-session parity for BAKES (TODO stage 1 finding, 2026-08-31):
|
|
129
|
+
# this VPC has no internet gateway and operator machines have no route
|
|
130
|
+
# to private IPs, so SSH-from-outside can never reach a build instance.
|
|
131
|
+
# When the runtime declares session_mechanism: ssm, packer connects
|
|
132
|
+
# through Session Manager instead: no public IP, an SSM-capable
|
|
133
|
+
# instance profile, and a user_data bootstrap that installs the agent
|
|
134
|
+
# on vendor AMIs that lack it (outbound via the subnet's NAT).
|
|
135
|
+
mech = str(getattr(model, "session_mechanism", "") or "").strip().lower()
|
|
136
|
+
ssm_profile = getattr(model, "session_instance_profile", None) or model.iam_instance_profile
|
|
137
|
+
if mech == "ssm" and ssm_profile:
|
|
138
|
+
c["ssh_interface"] = "session_manager"
|
|
139
|
+
c["iam_instance_profile"] = ssm_profile
|
|
140
|
+
c["associate_public_ip_address"] = False
|
|
141
|
+
c["ssh_timeout"] = "15m" # boot + agent install + SSM registration
|
|
142
|
+
# one HCL string line: escape embedded quotes and newlines (real
|
|
143
|
+
# newlines and bare quotes are illegal inside an HCL quoted string)
|
|
144
|
+
c["user_data"] = (_SSM_BOOTSTRAP.rstrip("\n").replace('"', '\\"')
|
|
145
|
+
.replace("\n", "\\n"))
|
|
146
|
+
# stage 63 items 22-23: the one resolver (the image's entry, the chain
|
|
147
|
+
# root's entry, config_username, the runtime's ssh_username, its
|
|
148
|
+
# default_config_username, the family's vendor user); the runtime's
|
|
149
|
+
# ssh_username used to override every entry here
|
|
150
|
+
from cs_image_system.base.bake_user import resolve_bake_user
|
|
151
|
+
from cs_image_system.base.global_context import GlobalTypeContext
|
|
152
|
+
c["ssh_username"] = resolve_bake_user(GlobalTypeContext(), image, runtime)
|
|
153
|
+
if networking.default_availability_zone:
|
|
154
|
+
c["availability_zone"] = networking.default_availability_zone
|
|
155
|
+
if model.iam_instance_profile and "iam_instance_profile" not in c:
|
|
156
|
+
# the SSM profile wins when both are set (stage 63 item 9, decided
|
|
157
|
+
# 2026-09-24): this assignment used to run after the SSM block and
|
|
158
|
+
# replace it, so a runtime with both baked under the wrong profile
|
|
159
|
+
c["iam_instance_profile"] = model.iam_instance_profile
|
|
160
|
+
lmap = {
|
|
161
|
+
# The SOURCE AMI's real root device, never a hardcoded name: RHEL
|
|
162
|
+
# roots are /dev/sda1, Debian/Amazon Linux roots are /dev/xvda, and
|
|
163
|
+
# on HVM the two names alias one device slot -- a mismatched mapping
|
|
164
|
+
# bakes an AMI carrying TWO root-slot volumes whose derived
|
|
165
|
+
# instances never boot (found live: the deb-11 chain).
|
|
166
|
+
"device_name": f'"{_root_device(image, psi)}"',
|
|
167
|
+
"volume_type": '"gp3"',
|
|
168
|
+
"delete_on_termination": True,
|
|
169
|
+
# stage 63: the one bake-disk rule the fingerprint also hashes
|
|
170
|
+
"volume_size": _bake_disk(image, runtime),
|
|
171
|
+
}
|
|
172
|
+
for k, v in c.items():
|
|
173
|
+
if isinstance(v, str) and not (v.startswith("var.") or v.startswith("data.")):
|
|
174
|
+
c[k] = f'"{v}"'
|
|
175
|
+
c["force_deregister"] = "!var.release"
|
|
176
|
+
doc = Builder()
|
|
177
|
+
doc.block("data", labels=['"amazon-ami"', f'"{runtime_data_id}"'], **query_assets)
|
|
178
|
+
source = doc.block("source", labels=[f'"{source_type}"', f'"{image.name}"'], **c)
|
|
179
|
+
source.block("launch_block_device_mappings", **lmap)
|
|
180
|
+
return hcl2.dumps(doc.build(), formatter_options=FO).splitlines()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from cs_image_system.base.models.provider_specific_image import ProviderSpecificImage
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AwsProviderSpecificImage(ProviderSpecificImage):
|
|
14
|
+
"""An image as AWS knows it: the identifier is an AMI id."""
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def csis_name(cls) -> str:
|
|
18
|
+
return "aws"
|
|
19
|
+
|
|
20
|
+
def get_query_assets(self) -> dict[str, Any] | None:
|
|
21
|
+
if self.is_resolved():
|
|
22
|
+
ret: dict[str, Any] = {
|
|
23
|
+
"filters": {
|
|
24
|
+
"image-id": f'"{self.identifier}"',
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
if self.owner:
|
|
28
|
+
ret["owners"] = [f'"{self.owner}"']
|
|
29
|
+
return ret
|
|
30
|
+
# Deferred: the image is produced by a build in this account; find the
|
|
31
|
+
# most recent image matching the output-name pattern (same shape as the
|
|
32
|
+
# chained-image lookup in the packer-ebs builder).
|
|
33
|
+
architecture = self.architecture if self.architecture else "x86_64"
|
|
34
|
+
return {
|
|
35
|
+
"owners": ['"self"'],
|
|
36
|
+
"filters": {
|
|
37
|
+
"name": f'"{self.deferred_name_pattern}*"',
|
|
38
|
+
"root-device-type": '"ebs"',
|
|
39
|
+
"virtualization-type": '"hvm"',
|
|
40
|
+
"architecture": f'"{architecture}"',
|
|
41
|
+
},
|
|
42
|
+
"most_recent": True,
|
|
43
|
+
}
|