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
This commit is contained in:
parent
9d7d6160bc
commit
98a115b96f
1
.gitignore
vendored
1
.gitignore
vendored
@ -43,6 +43,7 @@ coverage.xml
|
||||
|
||||
# Ansible
|
||||
*.retry
|
||||
hosts/inventory
|
||||
|
||||
# Installer artifacts
|
||||
offline-iac/
|
||||
|
||||
15
ansible.cfg
Normal file
15
ansible.cfg
Normal file
@ -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
|
||||
@ -5,3 +5,4 @@ pulumi-gcp
|
||||
pulumi-azure-native
|
||||
pulumi-alicloud
|
||||
PyYAML
|
||||
jinja2
|
||||
|
||||
124
scripts/dynamic_inventory.py
Executable file
124
scripts/dynamic_inventory.py
Executable file
@ -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()
|
||||
@ -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()
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user