Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

233 lines
8.0 KiB
Python

"""Bake a custom AMI with OpenSRE gateway pre-installed."""
from __future__ import annotations
import base64
import logging
import os
import shlex
import subprocess
import time
from pathlib import Path
from platform.deployment.aws.client import DEFAULT_REGION
from platform.deployment.aws.config import (
GATEWAY_AMI_GIT_REF_ENV,
GATEWAY_AMI_NAME_PREFIX,
GATEWAY_BUILDER_INSTANCE_TYPE,
SSM_MANAGED_POLICY_ARN,
)
from platform.deployment.aws.ec2 import (
create_image_from_instance,
create_instance_profile,
delete_instance_profile,
get_latest_al2023_ami,
launch_instance,
terminate_instance,
wait_for_running,
)
from platform.deployment.aws.ssm import run_ssm_shell_command, wait_for_ssm_registration
from platform.deployment.gateway.stack import (
GatewayStack,
get_stack,
save_ami_id,
)
logger = logging.getLogger(__name__)
_SERVICE_FILE = Path(__file__).parent / "systemd" / "opensre-gateway.service"
_OPENSRE_REPO = "Tracer-Cloud/opensre"
_BUILDER_ROLE_SUFFIX = "-bake-role"
_BUILDER_PROFILE_SUFFIX = "-bake-profile"
def _resolve_git_ref() -> str:
"""Return the git ref to bake into the AMI.
Resolution order:
1. ``OPENSRE_GATEWAY_GIT_REF`` environment variable.
2. ``git rev-parse HEAD`` in the local repo.
3. Falls back to ``"main"`` if git is not available.
"""
env_ref = os.getenv(GATEWAY_AMI_GIT_REF_ENV, "").strip()
if env_ref:
return env_ref
try:
result = subprocess.run(
["git", "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()
except Exception: # noqa: BLE001
logger.warning("Could not determine git HEAD; falling back to 'main'")
return "main"
def _format_ssm_failure(result: dict[str, str]) -> str:
"""Return a readable SSM failure message including stdout and stderr."""
status = result.get("status", "unknown")
stdout = result.get("stdout", "").strip()
stderr = result.get("stderr", "").strip()
parts = [f"status={status}"]
if stderr:
parts.append(f"stderr={stderr[-2000:]}")
if stdout:
parts.append(f"stdout={stdout[-2000:]}")
return ", ".join(parts)
def _build_install_commands(git_ref: str) -> list[str]:
"""Return the list of SSM shell commands that set up the gateway on the builder instance.
All setup is inlined here — no external bash script is downloaded or executed.
The systemd unit is base64-encoded from the local repo and decoded on the instance.
The opensre package is installed via pip's git+ protocol from GitHub.
"""
service_content = _SERVICE_FILE.read_text(encoding="utf-8")
encoded_service = base64.b64encode(service_content.encode()).decode("ascii")
# shlex.quote on the ref guards against shell injection for unusual ref names
install_url = f"git+https://github.com/{_OPENSRE_REPO}.git@{shlex.quote(git_ref)}"
return [
"set -euo pipefail",
# AL2023 ships curl-minimal; do not install the full curl package (conflicts)
"dnf install -y python3.12 python3.12-pip git",
# create system user and persistent data dirs
(
"id opensre &>/dev/null || useradd --system --create-home "
"--home-dir /var/lib/opensre-gateway --shell /sbin/nologin opensre"
),
"mkdir -p /var/lib/opensre-gateway/.opensre/gateway",
"chown -R opensre:opensre /var/lib/opensre-gateway",
"chmod 750 /var/lib/opensre-gateway",
# install opensre into an isolated venv
"python3.12 -m venv /opt/opensre/.venv",
f"/opt/opensre/.venv/bin/pip install --quiet {install_url}",
# smoke-check: verify the CLI is importable
"/opt/opensre/.venv/bin/opensre --help > /dev/null",
# env file directory (populated at deploy time, not bake time)
"mkdir -p /etc/opensre && chmod 750 /etc/opensre && chown root:opensre /etc/opensre",
# install systemd unit (base64-inlined from local repo)
(
f"echo {shlex.quote(encoded_service)} | base64 -d "
"> /etc/systemd/system/opensre-gateway.service"
),
"chmod 644 /etc/systemd/system/opensre-gateway.service",
"systemctl daemon-reload && systemctl enable opensre-gateway",
]
def bake_ami(
*,
region: str = DEFAULT_REGION,
ami_id_path: object = None,
) -> str:
"""Launch a builder instance, run the install commands, snapshot it into an AMI.
Returns the new AMI id.
"""
stack: GatewayStack = get_stack()
git_ref = _resolve_git_ref()
timestamp = int(time.time())
ami_name = f"{GATEWAY_AMI_NAME_PREFIX}-{git_ref[:12]}-{timestamp}"
print("=" * 60)
print(f"Baking gateway AMI for {stack.stack_name}")
print("=" * 60)
print()
print(f" Git ref : {git_ref}")
print(f" AMI name: {ami_name}")
print()
builder_role = f"{stack.stack_name}{_BUILDER_ROLE_SUFFIX}"
builder_profile = f"{stack.stack_name}{_BUILDER_PROFILE_SUFFIX}"
print("Creating builder IAM instance profile...")
profile_info = create_instance_profile(
role_name=builder_role,
profile_name=builder_profile,
stack_name=stack.stack_name,
region=region,
extra_policy_arns=[SSM_MANAGED_POLICY_ARN],
)
print(f" - Profile: {profile_info['ProfileName']}")
print("Looking up latest Amazon Linux 2023 AMI...")
base_ami = get_latest_al2023_ami(region)
print(f" - Base AMI: {base_ami}")
print(f"Launching builder instance ({GATEWAY_BUILDER_INSTANCE_TYPE})...")
instance = launch_instance(
ami_id=base_ami,
instance_profile_arn=profile_info["ProfileArn"],
stack_name=stack.stack_name,
instance_type=GATEWAY_BUILDER_INSTANCE_TYPE,
region=region,
)
instance_id = instance["InstanceId"]
print(f" - Instance ID: {instance_id}")
try:
print("Waiting for instance to start...")
wait_for_running(instance_id, region)
print(" - Running")
print("Waiting for SSM agent to register...")
wait_for_ssm_registration(instance_id, region)
print(" - SSM: Online")
print("Running install commands on builder instance (may take several minutes)...")
commands = _build_install_commands(git_ref)
result = run_ssm_shell_command(
instance_id=instance_id,
commands=commands,
region=region,
# Use generous poll limits — pip install can take a while
max_poll_attempts=120,
)
if result["status"] != "Success":
raise RuntimeError(
f"Install commands failed on {instance_id}: {_format_ssm_failure(result)}"
)
print(" - Install: OK")
print("Creating AMI (AWS will stop/snapshot/restart the instance)...")
image_id = create_image_from_instance(
instance_id=instance_id,
name=ami_name,
stack_name=stack.stack_name,
region=region,
)
print(f" - AMI: {image_id}")
finally:
print(f"Terminating builder instance {instance_id}...")
try:
terminate_instance(instance_id, region)
print(" - Terminated")
except Exception as exc: # noqa: BLE001
logger.warning("Failed to terminate builder instance %s: %s", instance_id, exc)
print(f"Deleting builder IAM profile {builder_profile}...")
try:
delete_instance_profile(builder_profile, builder_role, region)
print(" - Deleted")
except Exception as exc: # noqa: BLE001
logger.warning("Failed to delete builder profile %s: %s", builder_profile, exc)
kwargs = {"path": ami_id_path} if ami_id_path is not None else {}
save_ami_id(image_id, **kwargs) # type: ignore[arg-type]
print()
print("=" * 60)
print("AMI bake complete")
print(f" AMI id : {image_id}")
print(f" Git ref: {git_ref}")
print(" Run `make deploy-gateway` to launch an instance from this AMI.")
print("=" * 60)
print()
return image_id