Add playbooks/setup_awx_https.yml that deploys a host-level nginx reverse proxy in front of the AWX LoadBalancer service, terminating TLS on 443 and proxying to the existing HTTP service on 80. Why a reverse proxy and not in-pod TLS: the AWX Operator only wires nginx for HTTPS on the OpenShift Route passthrough code path (ingress_type: route + route_tls_termination_mechanism: passthrough), which requires the Route CRD and fails on k3s. The cluster has no ingress controller either. A host nginx proxy is the lowest-risk option and is trivially swappable when the internal CA / an ingress controller + cert-manager arrive. The self-signed cert (CN=tsys-awx.knel.net) carries SANs for the FQDN, short hostname, and both LAN and Tailscale IPs. Re-run the playbook to rotate the cert once the internal CA is rolled out. Also update scripts/awx_create_job_templates.py to use https by default and add AWX_VERIFY_TLS / AWX_CA_BUNDLE env vars so it works with the self-signed cert now and verifies properly once the internal CA bundle is distributed. 🤖 Generated with [Crush](https://github.com/charmassociates/crush) Assisted-by: GLM-5 via Crush <crush@charm.land>
191 lines
6.8 KiB
Python
191 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
awx_create_job_templates.py
|
|
|
|
Run this AFTER committing and pushing the KNELIAC code to the git remote.
|
|
It will:
|
|
1. Trigger a project sync in AWX (pulls latest code from git)
|
|
2. Wait for the sync to complete
|
|
3. Create the two job templates (Hello World + Setup New System)
|
|
4. Create the inventory source (auto-syncs hosts from inventory/hosts.yml)
|
|
|
|
Usage:
|
|
export AWX_PASSWORD='...'
|
|
python3 awx_create_job_templates.py
|
|
|
|
Configuration (environment variables):
|
|
AWX_URL AWX API base URL (default: https://tsys-awx.knel.net/api/v2)
|
|
AWX_USER AWX admin username (default: admin)
|
|
AWX_PASSWORD AWX admin password (REQUIRED - never committed to the repo)
|
|
AWX_PROJECT_ID, AWX_INVENTORY_ID, AWX_SCM_CRED_ID,
|
|
AWX_MACHINE_CRED_ID, AWX_ORG_ID resource IDs (sensible defaults below)
|
|
AWX_VERIFY_TLS "false" to skip TLS verification (for self-signed certs).
|
|
Default: verify against the system trust store.
|
|
AWX_CA_BUNDLE Path to a CA bundle file (e.g. internal CA) to verify against.
|
|
"""
|
|
|
|
import os
|
|
import ssl
|
|
import urllib.request, urllib.parse, json, base64, time, sys
|
|
|
|
API = os.environ.get("AWX_URL", "https://tsys-awx.knel.net/api/v2")
|
|
_AWX_USER = os.environ.get("AWX_USER", "admin")
|
|
_AWX_PASS = os.environ.get("AWX_PASSWORD")
|
|
if not _AWX_PASS:
|
|
sys.exit("ERROR: AWX_PASSWORD environment variable is not set. "
|
|
"Refusing to run — no credentials are stored in this repo.")
|
|
AUTH = base64.b64encode(f"{_AWX_USER}:{_AWX_PASS}".encode()).decode()
|
|
PROJECT_ID = int(os.environ.get("AWX_PROJECT_ID", "10"))
|
|
INVENTORY_ID = int(os.environ.get("AWX_INVENTORY_ID", "2"))
|
|
SCM_CRED_ID = int(os.environ.get("AWX_SCM_CRED_ID", "3"))
|
|
MACHINE_CRED_ID = int(os.environ.get("AWX_MACHINE_CRED_ID", "4"))
|
|
ORG_ID = int(os.environ.get("AWX_ORG_ID", "1"))
|
|
|
|
# TLS verification context. With a self-signed cert (until the internal CA
|
|
# bundle is distributed), set AWX_VERIFY_TLS=false. With an internal CA, point
|
|
# AWX_CA_BUNDLE at it and leave verification enabled.
|
|
if os.environ.get("AWX_VERIFY_TLS", "true").lower() == "false":
|
|
_SSL_CONTEXT = ssl._create_unverified_context()
|
|
elif os.environ.get("AWX_CA_BUNDLE"):
|
|
_SSL_CONTEXT = ssl.create_default_context(cafile=os.environ["AWX_CA_BUNDLE"])
|
|
else:
|
|
_SSL_CONTEXT = None # use urllib's default (system trust store)
|
|
|
|
def awx_req(method, endpoint, data=None):
|
|
url = f"{API}/{endpoint}"
|
|
body = json.dumps(data).encode() if data else None
|
|
req = urllib.request.Request(url, data=body, method=method)
|
|
req.add_header("Authorization", f"Basic {AUTH}")
|
|
req.add_header("Content-Type", "application/json")
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=30, context=_SSL_CONTEXT) as resp:
|
|
raw = resp.read()
|
|
return json.loads(raw) if raw else {}
|
|
except urllib.error.HTTPError as e:
|
|
err = e.read().decode()
|
|
print(f" ERROR {e.code}: {err[:300]}")
|
|
return None
|
|
|
|
def find_or_create(resource, name, payload):
|
|
encoded = urllib.parse.quote(name)
|
|
existing = awx_req("GET", f"{resource}/?name={encoded}")
|
|
if existing and existing.get("count", 0) > 0:
|
|
item = existing["results"][0]
|
|
print(f" EXISTS: [{item['id']}] {name}")
|
|
return item
|
|
result = awx_req("POST", f"{resource}/", payload)
|
|
if result:
|
|
print(f" CREATED: [{result.get('id')}] {name}")
|
|
return result
|
|
|
|
# Step 1: Sync the project
|
|
print("=" * 60)
|
|
print("STEP 1: Syncing project (pulling latest code from git)")
|
|
print("=" * 60)
|
|
sync = awx_req("POST", f"projects/{PROJECT_ID}/update/", {})
|
|
if not sync:
|
|
print("FAILED to trigger sync")
|
|
sys.exit(1)
|
|
update_id = sync.get("id")
|
|
print(f" Sync started: project_updates/{update_id}")
|
|
print(" Waiting...", end="", flush=True)
|
|
for i in range(30):
|
|
time.sleep(3)
|
|
print(".", end="", flush=True)
|
|
status = awx_req("GET", f"project_updates/{update_id}/")
|
|
s = status.get("status") if status else "?"
|
|
if s in ("successful", "failed", "error", "canceled"):
|
|
print(f" {s}")
|
|
break
|
|
else:
|
|
print(" timeout")
|
|
sys.exit(1)
|
|
|
|
if s != "successful":
|
|
print(f" Project sync failed: {s}")
|
|
print(" Make sure the code is committed and pushed to the git remote.")
|
|
sys.exit(1)
|
|
|
|
# Step 2: Verify playbooks are available
|
|
print("\n" + "=" * 60)
|
|
print("STEP 2: Verifying playbooks")
|
|
print("=" * 60)
|
|
playbooks = awx_req("GET", f"projects/{PROJECT_ID}/playbooks/")
|
|
if playbooks:
|
|
print(f" Found {len(playbooks)} playbook(s):")
|
|
for p in playbooks:
|
|
print(f" - {p}")
|
|
else:
|
|
print(" No playbooks found! Push your code and re-run.")
|
|
sys.exit(1)
|
|
|
|
# Step 3: Create inventory source
|
|
print("\n" + "=" * 60)
|
|
print("STEP 3: Creating inventory source")
|
|
print("=" * 60)
|
|
find_or_create("inventory_sources", "KNELIAC Fleet Source", {
|
|
"name": "KNELIAC Fleet Source",
|
|
"description": "Auto-synced from inventory/hosts.yml",
|
|
"inventory": INVENTORY_ID,
|
|
"source": "scm",
|
|
"source_project": PROJECT_ID,
|
|
"source_path": "inventory/hosts.yml",
|
|
"update_on_launch": True,
|
|
"overwrite": True,
|
|
"overwrite_vars": True,
|
|
})
|
|
|
|
# Step 4: Create job templates
|
|
print("\n" + "=" * 60)
|
|
print("STEP 4: Creating job templates")
|
|
print("=" * 60)
|
|
|
|
find_or_create("job_templates", "KNELIAC - Hello World", {
|
|
"name": "KNELIAC - Hello World",
|
|
"description": "AWX smoke test - verify connectivity to managed hosts",
|
|
"organization": ORG_ID,
|
|
"inventory": INVENTORY_ID,
|
|
"project": PROJECT_ID,
|
|
"playbook": "playbooks/hello_world.yml",
|
|
"credential": MACHINE_CRED_ID,
|
|
"verbosity": 1,
|
|
"forks": 5,
|
|
"ask_limit_on_launch": True,
|
|
})
|
|
|
|
find_or_create("job_templates", "KNELIAC - Setup New System", {
|
|
"name": "KNELIAC - Setup New System",
|
|
"description": "Full host provisioning (port of SetupNewSystem.sh)",
|
|
"organization": ORG_ID,
|
|
"inventory": INVENTORY_ID,
|
|
"project": PROJECT_ID,
|
|
"playbook": "playbooks/setup_new_system.yml",
|
|
"credential": MACHINE_CRED_ID,
|
|
"verbosity": 1,
|
|
"forks": 5,
|
|
"ask_limit_on_launch": True,
|
|
"ask_variables_on_launch": True,
|
|
})
|
|
|
|
print("\n" + "=" * 60)
|
|
print("ALL DONE - AWX is ready!")
|
|
print("=" * 60)
|
|
print("""
|
|
AWX URL: https://tsys-awx.knel.net (or https://100.91.39.53)
|
|
Login: $AWX_USER (password supplied via $AWX_PASSWORD env var)
|
|
Note: Self-signed cert for now — set AWX_VERIFY_TLS=false until the
|
|
internal CA bundle is distributed (then use AWX_CA_BUNDLE).
|
|
|
|
Resources created:
|
|
Credentials:
|
|
[3] KNELIAC Git Credential (Source Control - for git access)
|
|
[4] KNELIAC Host SSH Key (Machine - for fleet host access)
|
|
Project:
|
|
[10] KNELIAC (git repo, auto-syncs on launch)
|
|
Inventory:
|
|
[2] KNELIAC Fleet (auto-syncs hosts from inventory/hosts.yml)
|
|
Job Templates:
|
|
- KNELIAC - Hello World (smoke test, run first!)
|
|
- KNELIAC - Setup New System (full provisioning)
|
|
""")
|