From 98a115b96fa2800e43b712b7832a0583e885f73d Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 5 Apr 2025 12:38:31 +0800 Subject: [PATCH] feat(ansible): add dynamic inventory and ansible.cfg - Add ansible.cfg for plugin config - Enabled Pulumi passphrase auto-load in run.sh - Add scripts/dynamic_inventory.py with --list, --host, --export-static - Cleanup: remove legacy inventory.py --- .gitignore | 1 + ansible.cfg | 15 +++++ requirements.txt | 1 + scripts/dynamic_inventory.py | 124 +++++++++++++++++++++++++++++++++++ scripts/inventory.py | 73 --------------------- scripts/run.sh | 20 +++--- 6 files changed, 151 insertions(+), 83 deletions(-) create mode 100644 ansible.cfg create mode 100755 scripts/dynamic_inventory.py delete mode 100644 scripts/inventory.py diff --git a/.gitignore b/.gitignore index ba443955..99283d02 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ coverage.xml # Ansible *.retry +hosts/inventory # Installer artifacts offline-iac/ diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 00000000..4cee05d7 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,15 @@ +[inventory] +cache: yes +cache_plugin: ansible.builtin.jsonfile + +[defaults] +vault_password_file = ~/.vault_password +timeout = 10 +forks = 10 +poll_interval = 10 +transport = smart +gathering = smart +stdout_callback = skippy +host_key_checking = False +deprecation_warnings = False +ansible_python_interpreter=/usr/bin/python3 diff --git a/requirements.txt b/requirements.txt index 1fc49d7a..58fc5d7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pulumi-gcp pulumi-azure-native pulumi-alicloud PyYAML +jinja2 diff --git a/scripts/dynamic_inventory.py b/scripts/dynamic_inventory.py new file mode 100755 index 00000000..5e0f774a --- /dev/null +++ b/scripts/dynamic_inventory.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 + +import os +import sys +import json +import subprocess +import argparse +from pathlib import Path +from jinja2 import Template +from collections import defaultdict + +# ========== Pulumi Output ========== +def get_pulumi_outputs(pulumi_dir: Path): + try: + output = subprocess.check_output( + ["pulumi", "stack", "output", "--json"], + cwd=pulumi_dir, + env=os.environ + ) + return json.loads(output) + except subprocess.CalledProcessError as e: + print("[ERROR] Failed to get Pulumi outputs.") + print(e.output.decode(), file=sys.stderr) + return {} + except FileNotFoundError: + print("[ERROR] 'pulumi' command not found.") + sys.exit(1) + +# ========== Build JSON Inventory ========== +def build_inventory_from_outputs(outputs): + inventory = {"_meta": {"hostvars": {}}} + groups = defaultdict(list) + + for key, value in outputs.items(): + if key.endswith("_public_ip"): + name = key.replace("_public_ip", "") + ip = value + groups["all"].append(name) + inventory["_meta"]["hostvars"][name] = { + "ansible_host": ip, + "ansible_user": os.getenv("SSH_USER", "ubuntu"), + "cloud": "aws" # 默认值,可扩展为智能识别 + } + + for group, hosts in groups.items(): + inventory[group] = {"hosts": hosts} + + return inventory + +# ========== Static INI Inventory ========== +inventory_template = """\ +{% set max_len = groups['all'] | map(attribute='name') | map('length') | max %} +{% for group, hosts in groups.items() %} +[{{ group }}] +{% for host in hosts -%} +{{ "{:<{width}}".format(host.name, width=max_len) }} ansible_host={{ host.ip }} +{% endfor %} + +{% endfor -%} +[all:vars] +ansible_port=22 +ansible_ssh_user={{ ssh_user }} +ansible_ssh_private_key_file=~/.ssh/id_rsa +ansible_host_key_checking=False +""" + +def build_static_inventory(outputs): + groups = defaultdict(list) + for key, value in outputs.items(): + if key.endswith("_public_ip"): + name = key.replace("_public_ip", "") + groups["all"].append({"name": name, "ip": value}) + return groups + +# ========== Main ========== +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--list', action='store_true', help="Output dynamic inventory (JSON)") + parser.add_argument('--host', help="Output host-specific variables") + parser.add_argument('--export-static', action='store_true', help="Export static inventory to hosts/inventory") + parser.add_argument('--pulumi-dir', default="iac_modules/pulumi", help="Path to Pulumi stack directory") + parser.add_argument('--passphrase-file', default="~/.pulumi-passphrase", help="Path to Pulumi config passphrase file") + args = parser.parse_args() + + # 解析目录 + base_dir = Path(__file__).resolve().parent.parent + pulumi_dir = (base_dir / args.pulumi_dir).resolve() + passphrase_file = Path(args.passphrase_file).expanduser().resolve() + + # 设置默认 Pulumi 密码环境变量 + if "PULUMI_CONFIG_PASSPHRASE_FILE" not in os.environ and "PULUMI_CONFIG_PASSPHRASE" not in os.environ: + if not passphrase_file.exists(): + print(f"[ERROR] Pulumi passphrase file not found at {passphrase_file}") + sys.exit(1) + os.environ["PULUMI_CONFIG_PASSPHRASE_FILE"] = str(passphrase_file) + + # 获取 Pulumi 输出 + outputs = get_pulumi_outputs(pulumi_dir) + inventory = build_inventory_from_outputs(outputs) + + if args.list: + print(json.dumps(inventory, indent=2)) + return + + if args.host: + hostvars = inventory.get('_meta', {}).get('hostvars', {}) + print(json.dumps(hostvars.get(args.host, {}), indent=2)) + return + + if args.export_static: + groups = build_static_inventory(outputs) + ssh_user = os.getenv("SSH_USER", "ubuntu") + template = Template(inventory_template) + output = template.render(groups=groups, ssh_user=ssh_user) + os.makedirs("hosts", exist_ok=True) + with open("hosts/inventory", "w") as f: + f.write(output) + print("✅ Static inventory written to hosts/inventory") + return + + print(json.dumps({})) # fallback + +if __name__ == "__main__": + main() diff --git a/scripts/inventory.py b/scripts/inventory.py deleted file mode 100644 index 7a634481..00000000 --- a/scripts/inventory.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 - -import json -import subprocess -import os -import yaml -from collections import defaultdict - -def get_pulumi_outputs(): - output = subprocess.check_output(["pulumi", "stack", "output", "--json"]) - return json.loads(output) - -def merge_instance_config(config_dir="config"): - merged = {} - for fname in os.listdir(config_dir): - if fname.endswith(".yaml"): - with open(os.path.join(config_dir, fname)) as f: - data = yaml.safe_load(f) - if isinstance(data, dict): - merged.update(data) - return merged.get("instances", []) - -def build_inventory(pulumi_outputs, instance_cfgs): - inventory = {"_meta": {"hostvars": {}}} - groups = defaultdict(list) - - for inst in instance_cfgs: - name = inst["name"] - public_ip = pulumi_outputs.get(f"{name}_ip") - - if not public_ip: - continue # skip not created instances - - # 默认分组:all - groups["all"].append(name) - - # 根据 subnet 或 lifecycle 添加分组 - if "subnet" in inst: - groups[inst["subnet"]].append(name) - if "lifecycle" in inst: - groups[inst["lifecycle"]].append(name) - - # hostvars - inventory["_meta"]["hostvars"][name] = { - "ansible_host": public_ip, - "ansible_user": "ubuntu", - "instance_type": inst.get("type"), - "ttl": inst.get("ttl", "none"), - "lifecycle": inst.get("lifecycle", "ondemand"), - } - - # 将分组注入 inventory - for group, hosts in groups.items(): - inventory[group] = {"hosts": hosts} - - return inventory - -def main(): - import argparse - parser = argparse.ArgumentParser() - parser.add_argument('--list', action='store_true') - args = parser.parse_args() - - if args.list: - pulumi_data = get_pulumi_outputs() - instance_cfgs = merge_instance_config() - inventory = build_inventory(pulumi_data, instance_cfgs) - print(json.dumps(inventory, indent=2)) - else: - print(json.dumps({})) - -if __name__ == "__main__": - main() diff --git a/scripts/run.sh b/scripts/run.sh index cc46503a..a42311f4 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -17,16 +17,16 @@ ANSIBLE_DIR="${PROJECT_ROOT}/ansible" # ================================ # ✅ 自动加载 Pulumi passphrase # ================================ -#export PULUMI_CONFIG_PASSPHRASE_FILE="${PULUMI_CONFIG_PASSPHRASE_FILE:-$HOME/.pulumi-passphrase}" -# -#if [ ! -f "$PULUMI_CONFIG_PASSPHRASE_FILE" ]; then -# echo "⚠️ 未检测到 Pulumi 密码文件: $PULUMI_CONFIG_PASSPHRASE_FILE" -# echo "请先创建该文件并写入 passphrase,例如:" -# echo " echo 'changeme123' > ~/.pulumi-passphrase && chmod 600 ~/.pulumi-passphrase" -# exit 1 -#else -# echo "🔐 Pulumi 密码文件已加载: $PULUMI_CONFIG_PASSPHRASE_FILE" -#fi +export PULUMI_CONFIG_PASSPHRASE_FILE="${PULUMI_CONFIG_PASSPHRASE_FILE:-$HOME/.pulumi-passphrase}" + +if [ ! -f "$PULUMI_CONFIG_PASSPHRASE_FILE" ]; then + echo "⚠️ 未检测到 Pulumi 密码文件: $PULUMI_CONFIG_PASSPHRASE_FILE" + echo "请先创建该文件并写入 passphrase,例如:" + echo " echo 'changeme123' > ~/.pulumi-passphrase && chmod 600 ~/.pulumi-passphrase" + exit 1 +else + echo "🔐 Pulumi 密码文件已加载: $PULUMI_CONFIG_PASSPHRASE_FILE" +fi # ========== 参数解析 ========== if [[ -n "$1" && "$1" != up && "$1" != down && "$1" != delete && "$1" != export && "$1" != import && "$1" != init && "$1" != ansible && "$1" != help ]]; then