feat: initial commit
- have been working on this for some time now
This commit is contained in:
@@ -0,0 +1 @@
|
||||
jinja2
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
networking::interfaces:
|
||||
{{ interface }}:
|
||||
ipaddress: {{ ipaddress }}
|
||||
|
||||
networking::routes:
|
||||
default:
|
||||
gateway: {{ gateway }}
|
||||
@@ -0,0 +1,170 @@
|
||||
import json
|
||||
import argparse
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from jinja2 import Template
|
||||
|
||||
### ========== GITOPS FUNCTIONS ==========
|
||||
|
||||
def run_command(command, cwd=None):
|
||||
result = subprocess.run(command, cwd=cwd, shell=True, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
raise Exception(f"Command '{command}' failed: {result.stderr}")
|
||||
return result.stdout.strip()
|
||||
|
||||
def clone(repo_url, clone_path: Path):
|
||||
run_command(f"git clone {repo_url} {clone_path}")
|
||||
|
||||
def checkout_base_branch(clone_path: Path, base_branch: str = "develop"):
|
||||
print(f"🔁 Checking out base branch: {base_branch}")
|
||||
run_command(f"git checkout {base_branch}", cwd=clone_path)
|
||||
|
||||
def checkout_branch(clone_path: Path, branch_name: str):
|
||||
run_command(f"git checkout -b {branch_name}", cwd=clone_path)
|
||||
|
||||
def add(clone_path: Path, file_path: Path):
|
||||
rel_path = file_path.relative_to(clone_path)
|
||||
run_command(f"git add {rel_path}", cwd=clone_path)
|
||||
|
||||
def commit(clone_path: Path, commit_message: str):
|
||||
run_command(f'git commit -m "{commit_message}"', cwd=clone_path)
|
||||
|
||||
def push(clone_path: Path, branch_name: str):
|
||||
run_command(f"git push origin {branch_name}", cwd=clone_path)
|
||||
|
||||
def create_file_from_template(file_path: Path, template_content: str, context: dict, dryrun: bool):
|
||||
template = Template(template_content)
|
||||
rendered = template.render(context)
|
||||
if dryrun:
|
||||
print(f"\n📝 Would write to {file_path}:\n{rendered}")
|
||||
else:
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.write_text(rendered)
|
||||
|
||||
def cleanup(clone_path: Path):
|
||||
run_command(f"rm -rf {clone_path}")
|
||||
|
||||
### ========== NODE OPERATION ==========
|
||||
|
||||
def process_node(vmname: str, ipaddress: str, gateway: str, clone_path: Path,
|
||||
commit_template: str, file_template: str, dryrun: bool):
|
||||
|
||||
file_rel_path = Path(f"hieradata/nodes/{vmname}.yaml")
|
||||
file_path = clone_path / file_rel_path
|
||||
branch_name = f"autonode/{vmname}"
|
||||
|
||||
if file_path.exists() and not dryrun:
|
||||
print(f"⚠️ Skipping {vmname}: {file_path} already exists.")
|
||||
return
|
||||
|
||||
print(f"\n🌿 Creating branch: {branch_name}")
|
||||
checkout_branch(clone_path, branch_name)
|
||||
|
||||
print(f"📝 Rendering YAML for {vmname}")
|
||||
create_file_from_template(
|
||||
file_path,
|
||||
file_template,
|
||||
{
|
||||
"ipaddress": ipaddress,
|
||||
"gateway": gateway,
|
||||
"interface": "eth0"
|
||||
},
|
||||
dryrun
|
||||
)
|
||||
|
||||
if dryrun:
|
||||
print(f"💤 Dry run: skipping add/commit/push for {vmname}")
|
||||
return
|
||||
|
||||
print(f"➕ Adding {file_rel_path}")
|
||||
add(clone_path, file_path)
|
||||
|
||||
commit_msg = Template(commit_template).render({"vmname": vmname})
|
||||
print(f"✅ Committing: {commit_msg}")
|
||||
commit(clone_path, commit_msg)
|
||||
|
||||
print(f"🚀 Pushing {branch_name}")
|
||||
push(clone_path, branch_name)
|
||||
|
||||
|
||||
def load_broken_tf_outputs(file_path: Path):
|
||||
"""Handles newline-separated JSON objects (non-standard tf_outputs.json format)."""
|
||||
objects = []
|
||||
buffer = ""
|
||||
for line in file_path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
buffer += line
|
||||
if buffer.endswith("}"):
|
||||
try:
|
||||
obj = json.loads(buffer)
|
||||
objects.append(obj)
|
||||
buffer = ""
|
||||
except json.JSONDecodeError:
|
||||
buffer += " " # accumulate more lines until it's valid
|
||||
return objects
|
||||
|
||||
### ========== MAIN CLI SCRIPT ==========
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate Hiera node YAMLs and push to Git")
|
||||
parser.add_argument("--output-json", required=True, type=Path, help="Terragrunt JSON outputs")
|
||||
parser.add_argument("--repo-url", required=True, help="Git repo URL")
|
||||
parser.add_argument("--clone-path", required=True, type=Path, help="Temp clone path")
|
||||
parser.add_argument("--commit-template", required=True, help="Commit message Jinja2 template")
|
||||
parser.add_argument("--file-template", required=True, type=Path, help="Path to Jinja2 YAML template")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Do not write or push, just preview")
|
||||
parser.add_argument("--base-branch", default="develop", help="Base branch to branch off (default: develop)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.clone_path.exists():
|
||||
print(f"🧹 Removing existing clone at {args.clone_path}")
|
||||
cleanup(args.clone_path)
|
||||
|
||||
print(f"📥 Cloning repo to {args.clone_path}")
|
||||
clone(args.repo_url, args.clone_path)
|
||||
|
||||
file_template = args.file_template.read_text()
|
||||
# Use loader
|
||||
parsed_objects = load_broken_tf_outputs(args.output_json)
|
||||
|
||||
# Flatten into merged format using hostnames
|
||||
merged_outputs = {}
|
||||
for obj in parsed_objects:
|
||||
if "vm_metadata" in obj and "value" in obj["vm_metadata"]:
|
||||
hostname = obj["vm_metadata"]["value"]["hostname"]
|
||||
merged_outputs[f"vm_metadata_{hostname}"] = obj["vm_metadata"]
|
||||
|
||||
for module_path, data in merged_outputs.items():
|
||||
if "value" not in data:
|
||||
print(f"⏭️ Skipping {module_path}: missing 'value'")
|
||||
continue
|
||||
|
||||
node = data["value"]
|
||||
vmname = node["hostname"]
|
||||
ip = node["ipaddress"]
|
||||
gw = node["gateway"]
|
||||
|
||||
checkout_base_branch(args.clone_path, args.base_branch)
|
||||
|
||||
print(f"\n🔧 Processing {vmname} ({ip})")
|
||||
process_node(
|
||||
vmname=vmname,
|
||||
ipaddress=ip,
|
||||
gateway=gw,
|
||||
clone_path=args.clone_path,
|
||||
commit_template=args.commit_template,
|
||||
file_template=file_template,
|
||||
dryrun=args.dry_run
|
||||
)
|
||||
|
||||
if not args.dry_run:
|
||||
print(f"\n🧹 Cleaning up: {args.clone_path}")
|
||||
cleanup(args.clone_path)
|
||||
|
||||
print("\n🏁 All done!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,4 @@
|
||||
python-hcl2==7.2.0
|
||||
pyyaml==6.0.2
|
||||
rich==14.0.0
|
||||
typer==0.15.3
|
||||
@@ -0,0 +1,73 @@
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
import hcl2
|
||||
from collections import defaultdict
|
||||
from rich.console import Console
|
||||
from rich.tree import Tree
|
||||
import typer
|
||||
import re
|
||||
|
||||
# Define the root paths
|
||||
INSTANCES_DIR = Path("config/instances")
|
||||
|
||||
def extract_node_name(hcl_path):
|
||||
text = hcl_path.read_text()
|
||||
match = re.search(r'node_name\s*=\s*"([^"]+)"', text)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
# Function to extract cobbler_mgmt_classes and profiles from config.yaml
|
||||
def extract_config_data(config_path):
|
||||
with config_path.open("r") as f:
|
||||
config = yaml.safe_load(f)
|
||||
return (
|
||||
config.get("cobbler_mgmt_classes", []),
|
||||
config.get("profiles", []),
|
||||
)
|
||||
|
||||
# Build a dictionary mapping node_name to instances and their metadata
|
||||
def build_node_tree():
|
||||
tree_data = defaultdict(list)
|
||||
for instance_dir in INSTANCES_DIR.iterdir():
|
||||
if not instance_dir.is_dir() or instance_dir.name in {"template"}:
|
||||
continue
|
||||
|
||||
config_path = instance_dir / "config.yaml"
|
||||
hcl_path = instance_dir / "terragrunt.hcl"
|
||||
|
||||
if not config_path.exists() or not hcl_path.exists():
|
||||
continue
|
||||
|
||||
node_name = extract_node_name(hcl_path)
|
||||
if not node_name:
|
||||
continue
|
||||
|
||||
classes, profiles = extract_config_data(config_path)
|
||||
tree_data[node_name].append({
|
||||
"instance": instance_dir.name,
|
||||
"classes": classes,
|
||||
"profiles": profiles
|
||||
})
|
||||
return tree_data
|
||||
|
||||
# CLI using Typer
|
||||
app = typer.Typer()
|
||||
console = Console()
|
||||
|
||||
@app.command()
|
||||
def show():
|
||||
data = build_node_tree()
|
||||
root = Tree("📦 [bold blue]Node Overview[/bold blue]")
|
||||
for node, instances in sorted(data.items()):
|
||||
node_branch = root.add(f"[bold green]{node}[/bold green]")
|
||||
for inst in sorted(instances, key=lambda x: x['instance']):
|
||||
inst_branch = node_branch.add(f"[cyan]{inst['instance']}[/cyan]")
|
||||
if inst['classes']:
|
||||
inst_branch.add(f"🛠️ classes: {', '.join(inst['classes'])}")
|
||||
if inst['profiles']:
|
||||
inst_branch.add(f"📋 profiles: {', '.join(inst['profiles'])}")
|
||||
console.print(root)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Find repo root
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
||||
|
||||
if [ -z "$REPO_ROOT" ]; then
|
||||
echo "❗ Could not detect Git repo root. Are you inside a Git repo?"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Go up three directories from the current folder
|
||||
pushd ../../../
|
||||
INSTANCE_DIR=$(pwd)
|
||||
INSTANCE_NAME="$(basename $(pwd))"
|
||||
popd
|
||||
|
||||
echo "🔎 Detected instance: $INSTANCE_NAME"
|
||||
|
||||
# Find the real terragrunt.hcl location
|
||||
TERRAGRUNT_HCL="${INSTANCE_DIR}/terragrunt.hcl"
|
||||
|
||||
if [ ! -f "$TERRAGRUNT_HCL" ]; then
|
||||
echo "❗ terragrunt.hcl not found at expected location: $TERRAGRUNT_HCL"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# Extract node_name from terragrunt.hcl
|
||||
NODE_NAME=$(grep 'node_name *= *' "$TERRAGRUNT_HCL" | sed -E 's/.*=\s*"([^"]+)".*/\1/')
|
||||
|
||||
if [ -z "$NODE_NAME" ]; then
|
||||
echo "❗ node_name not found in $TERRAGRUNT_HCL"
|
||||
exit 4
|
||||
fi
|
||||
|
||||
# Set config file path
|
||||
YAML_FILE="${REPO_ROOT}/config/nodes/${NODE_NAME}/config.yaml"
|
||||
|
||||
if [ ! -f "$YAML_FILE" ]; then
|
||||
echo "❗ Config file $YAML_FILE not found!"
|
||||
exit 5
|
||||
fi
|
||||
|
||||
echo "✔️ Exporting environment variables from $YAML_FILE"
|
||||
|
||||
# Export vars
|
||||
export NODE_ADDR=$(yq eval '.node_addr' "$YAML_FILE")
|
||||
export NODE_PORT=$(yq eval '.node_port' "$YAML_FILE")
|
||||
export NODE_NAME="${NODE_NAME}"
|
||||
|
||||
# Echo for debugging
|
||||
echo "NODE_ADDR=$NODE_ADDR"
|
||||
echo "NODE_PORT=$NODE_PORT"
|
||||
echo "NODE_NAME=$NODE_NAME"
|
||||
Reference in New Issue
Block a user