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.
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2026 Mykel Alvis <mykelalvis@infrastructurebuilder.org>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.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
+ }