mirror of
https://gitlab.sectorq.eu/jaydee/ansible_executor.git
synced 2026-09-17 01:22:54 +02:00
Initial commit
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
# Vault
|
||||||
|
VAULT_ADDR=https://vault.sectorq.eu
|
||||||
|
VAULT_TOKEN=
|
||||||
|
# GitLab
|
||||||
|
GITLAB_URL=https://gitlab.sectorq.eu/jaydee/ansible.git
|
||||||
|
GITLAB_BRANCH=main
|
||||||
|
|
||||||
|
# Vault secret paths (KV v2)
|
||||||
|
VAULT_GITLAB_SECRET=secret/data/gitlab
|
||||||
|
VAULT_GITLAB_FIELD=token
|
||||||
|
|
||||||
|
VAULT_ANSIBLE_SECRET=secret/data/ansible
|
||||||
|
VAULT_ANSIBLE_FIELD=ansible_vault_password
|
||||||
|
|
||||||
|
# Local clone directory
|
||||||
|
CLONE_DIR=/tmp/ansible-repo
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
|
||||||
|
stages: # List of stages for jobs, and their order of execution
|
||||||
|
- notify1
|
||||||
|
- lint
|
||||||
|
- build
|
||||||
|
- clean
|
||||||
|
- notify
|
||||||
|
variables:
|
||||||
|
GIT_SSH_COMMAND: "ssh -i /home/gitlab-runner/.ssh/id_rsa -o IdentitiesOnly=yes"
|
||||||
|
notify1:
|
||||||
|
stage: notify1 # Should be in a later stage than the job that might fail
|
||||||
|
when: on_success # <-- This is the key keyword
|
||||||
|
script:
|
||||||
|
- column=':'
|
||||||
|
- echo "${flow_id}"
|
||||||
|
- curl -XPOST http://192.168.77.101:8123/api/webhook/voice-notifications-tC_8YKxMJIAaQRV5riKuC7Zl --data-raw 'message=ansible executor build job started'
|
||||||
|
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/ansible_executor.tmp
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_MESSAGE =~ /build/'
|
||||||
|
lint:
|
||||||
|
stage: lint
|
||||||
|
image: r.sectorq.eu/jaydee/builder:latest
|
||||||
|
before_script:
|
||||||
|
- export PATH="$PATH:/home/gitlab-runner/.local/bin"
|
||||||
|
# - echo "PATH is now: $PATH"
|
||||||
|
script:
|
||||||
|
- flake8 .
|
||||||
|
- black --check .
|
||||||
|
- pylint main.py
|
||||||
|
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/ansible_executor.tmp
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_MESSAGE =~ /lint/'
|
||||||
|
build-job: # This job runs in the build stage, which runs first.
|
||||||
|
stage: build
|
||||||
|
image: r.sectorq.eu/jaydee/builder:latest
|
||||||
|
script:
|
||||||
|
- rm -rf build dist *.spec
|
||||||
|
- pyinstaller --onefile --clean -n ansr main.py
|
||||||
|
#- scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/portainer jd@192.168.80.222:/myapps/bin/ || true
|
||||||
|
- scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/ansr jd@192.168.77.12:/myapps/bin/ || true
|
||||||
|
- curl -F "file=@dist/ansr" https://myapps.sectorq.eu/
|
||||||
|
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/ansible_executor.tmp
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- dist/
|
||||||
|
expire_in: 1 week
|
||||||
|
# - column=":"
|
||||||
|
# - echo "${flow_id}"
|
||||||
|
# - curl -X POST https://kestra.sectorq.eu/api/v1/executions/webhook/jaydee/ansible-all/${flow_id} -d '{"tag":["proxmox"],"target":["servers"]}' -H "Content-Type${column} application/json"
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_MESSAGE =~ /build/'
|
||||||
|
|
||||||
|
|
||||||
|
build-job-arm: # This job runs in the build stage, which runs first.
|
||||||
|
stage: build
|
||||||
|
image: r.sectorq.eu/jaydee/builder-arm:latest
|
||||||
|
script:
|
||||||
|
- rm -rf build dist *.spec
|
||||||
|
- pyinstaller --onefile --clean -n ansr_arm main.py
|
||||||
|
#- scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/portainer jd@192.168.80.222:/myapps/bin/ || true
|
||||||
|
- scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dist/ansr_arm jd@192.168.77.12:/myapps/bin/ || true
|
||||||
|
- curl -F "file=@dist/ansr_arm" https://myapps.sectorq.eu/
|
||||||
|
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/ansible_executor.tmp
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- dist/
|
||||||
|
expire_in: 1 week
|
||||||
|
# - column=":"
|
||||||
|
# - echo "${flow_id}"
|
||||||
|
# - curl -X POST https://kestra.sectorq.eu/api/v1/executions/webhook/jaydee/ansible-all/${flow_id} -d '{"tag":["proxmox"],"target":["servers"]}' -H "Content-Type${column} application/json"
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_MESSAGE =~ /build-all/'
|
||||||
|
|
||||||
|
clean-job: # This job runs in the build stage, which runs first.
|
||||||
|
stage: clean
|
||||||
|
script:
|
||||||
|
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/ansible_executor.tmp
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_MESSAGE =~ /build/'
|
||||||
|
cleanup_on_failure_job:
|
||||||
|
stage: clean # Should be in a later stage than the job that might fail
|
||||||
|
when: on_failure # <-- This is the key keyword
|
||||||
|
script:
|
||||||
|
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/ansible_executor.tmp
|
||||||
|
notify:
|
||||||
|
stage: notify # Should be in a later stage than the job that might fail
|
||||||
|
when: on_success # <-- This is the key keyword
|
||||||
|
script:
|
||||||
|
- column=':'
|
||||||
|
- echo "${flow_id}"
|
||||||
|
- curl -XPOST http://192.168.77.101:8123/api/webhook/voice-notifications-tC_8YKxMJIAaQRV5riKuC7Zl --data-raw 'message=ansible executor build job completed'
|
||||||
|
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/ansible_executor.tmp
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_MESSAGE =~ /build/'
|
||||||
|
notify2:
|
||||||
|
stage: notify # Should be in a later stage than the job that might fail
|
||||||
|
when: on_failure # <-- This is the key keyword
|
||||||
|
script:
|
||||||
|
- column=':'
|
||||||
|
- echo "${flow_id}"
|
||||||
|
- curl -XPOST http://192.168.77.101:8123/api/webhook/voice-notifications-tC_8YKxMJIAaQRV5riKuC7Zl --data-raw 'message=ansible executor build job failed'
|
||||||
|
- rm -rf /home/gitlab-runner/builds/1fLwHSKm2/0/jaydee/ansible_executor.tmp
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_MESSAGE =~ /build/'
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import hvac
|
||||||
|
from git import Repo
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from prompt_toolkit.shortcuts import checkboxlist_dialog, radiolist_dialog, message_dialog
|
||||||
|
# from ansible.parsing.dataloader import DataLoader
|
||||||
|
# from ansible.inventory.manager import InventoryManager
|
||||||
|
|
||||||
|
VERSION = "0.0.14"
|
||||||
|
|
||||||
|
# ================= LOAD ENV =================
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
VAULT_ADDR = os.getenv("VAULT_ADDR", "https://vault.sectorq.eu")
|
||||||
|
VAULT_TOKEN = os.getenv("VAULT_TOKEN") or os.environ["VAULT_TOKEN"]
|
||||||
|
|
||||||
|
GITLAB_URL = os.getenv("GITLAB_URL", "https://gitlab.sectorq.eu/jaydee/ansible.git")
|
||||||
|
GITLAB_BRANCH = os.getenv("GITLAB_BRANCH", "main")
|
||||||
|
|
||||||
|
VAULT_GITLAB_PATH = os.getenv("VAULT_GITLAB_PATH", "secret/data/gitlab")
|
||||||
|
VAULT_GITLAB_FIELD = os.getenv("VAULT_GITLAB_FIELD", "token")
|
||||||
|
|
||||||
|
VAULT_ANSIBLE_PATH = os.getenv("VAULT_ANSIBLE_SECRET", "secret/data/ansible")
|
||||||
|
VAULT_ANSIBLE_VAULT_FIELD = os.getenv("VAULT_ANSIBLE_VAULT_FIELD", "vault")
|
||||||
|
VAULT_ANSIBLE_SSH_FIELD = os.getenv("VAULT_ANSIBLE_SSH_FIELD", "ssh_key")
|
||||||
|
|
||||||
|
CLONE_DIR = Path(os.getenv("CLONE_DIR", "/tmp/ansible-repo"))
|
||||||
|
|
||||||
|
# ================= VAULT =================
|
||||||
|
def get_vault_client():
|
||||||
|
if not VAULT_ADDR or not VAULT_TOKEN:
|
||||||
|
sys.exit("Vault config missing in .env")
|
||||||
|
client = hvac.Client(url=VAULT_ADDR, token=VAULT_TOKEN)
|
||||||
|
if not client.is_authenticated():
|
||||||
|
sys.exit("Vault authentication failed")
|
||||||
|
return client
|
||||||
|
|
||||||
|
def read_kv2_secret(client, path, field):
|
||||||
|
clean_path = path.replace("secret/data/", "")
|
||||||
|
secret = client.secrets.kv.v2.read_secret_version(path=clean_path)
|
||||||
|
return secret["data"]["data"][field]
|
||||||
|
|
||||||
|
# ================= GIT =================
|
||||||
|
def clone_or_update_repo(repo_url, branch, token):
|
||||||
|
repo_url_auth = repo_url.replace("https://", f"https://oauth2:{token}@")
|
||||||
|
if CLONE_DIR.exists():
|
||||||
|
print("Updating repository...")
|
||||||
|
repo = Repo(CLONE_DIR)
|
||||||
|
repo.git.checkout(branch)
|
||||||
|
repo.remotes.origin.pull()
|
||||||
|
else:
|
||||||
|
print("Cloning repository...")
|
||||||
|
Repo.clone_from(repo_url_auth, CLONE_DIR, branch=branch)
|
||||||
|
|
||||||
|
# ================= MENU =================
|
||||||
|
def select_inventory():
|
||||||
|
inventories = ["hosts_init.yml", "hosts_roles.yml"]
|
||||||
|
result = radiolist_dialog(
|
||||||
|
title=f"Inventory Selection - Version {VERSION}",
|
||||||
|
text="Select inventory file:",
|
||||||
|
values=[(inv, inv) for inv in inventories],
|
||||||
|
).run()
|
||||||
|
if not result:
|
||||||
|
sys.exit(0)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def select_roles():
|
||||||
|
roles_path = CLONE_DIR / "roles"
|
||||||
|
if not roles_path.exists():
|
||||||
|
return []
|
||||||
|
roles = sorted([r.name for r in roles_path.iterdir() if r.is_dir()])
|
||||||
|
result = checkboxlist_dialog(
|
||||||
|
title="Role Selection",
|
||||||
|
text="Select roles to execute (for info/log only):",
|
||||||
|
values=[(r, r) for r in roles],
|
||||||
|
).run()
|
||||||
|
return result or []
|
||||||
|
|
||||||
|
def select_limit():
|
||||||
|
# Path to your inventory file
|
||||||
|
inventory_path = f"{CLONE_DIR}/hosts_roles.yml"
|
||||||
|
|
||||||
|
# loader = DataLoader()
|
||||||
|
# inventory = InventoryManager(loader=loader, sources=[inventory_path])
|
||||||
|
|
||||||
|
# Get all hosts
|
||||||
|
# hosts = inventory.get_hosts()
|
||||||
|
|
||||||
|
# for host in hosts:
|
||||||
|
# print(host.name)
|
||||||
|
limits = [
|
||||||
|
("all", "All hosts"),
|
||||||
|
("m-server.home.lan", "m-server"),
|
||||||
|
("morefine.home.lan", "morefine"),
|
||||||
|
("asus.home.lan", "asus"),
|
||||||
|
("nas.home.lan", "nas"),
|
||||||
|
("rpi4.home.lan", "rpi4"),
|
||||||
|
("rpi5.home.lan", "rpi5"),
|
||||||
|
("debian13", "Debian 13"),
|
||||||
|
("ubuntu24", "Ubuntu 24"),
|
||||||
|
("ubuntu24s", "Ubuntu 24 Server"),
|
||||||
|
("ubuntu26s", "Ubuntu 26 Server"),
|
||||||
|
("rocky9", "Rocky 9"),
|
||||||
|
("rocky10", "Rocky 10"),
|
||||||
|
("alma10", "AlmaLinux 10"),
|
||||||
|
("custom", "Custom hosts (comma-separated)"),
|
||||||
|
]
|
||||||
|
result = checkboxlist_dialog(
|
||||||
|
title="Inventory Limit",
|
||||||
|
text="Select host limit:",
|
||||||
|
values=limits,
|
||||||
|
).run()
|
||||||
|
|
||||||
|
if result and "custom" in result:
|
||||||
|
custom_hosts = input("Enter custom hosts (comma-separated): ")
|
||||||
|
result = [r for r in result if r != "custom"] + [custom_hosts]
|
||||||
|
if not result:
|
||||||
|
sys.exit(0)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# ================= RUN ANSIBLE =================
|
||||||
|
def run_ansible_playbook(inventory, limit, roles=None, vault_password=None, ssh_key=None):
|
||||||
|
# create ssh key file
|
||||||
|
content = "Hello world"
|
||||||
|
|
||||||
|
file_path = f"{CLONE_DIR}/ssh_key.pem"
|
||||||
|
|
||||||
|
# create file
|
||||||
|
with open(file_path, "w") as f:
|
||||||
|
f.write(ssh_key + "\n")
|
||||||
|
# set permissions (rw-r--r--)
|
||||||
|
os.chmod(file_path, 0o600)
|
||||||
|
playbook_file = CLONE_DIR / "all.yml"
|
||||||
|
if not playbook_file.exists():
|
||||||
|
sys.exit(f"Playbook {playbook_file} not found!")
|
||||||
|
|
||||||
|
# Write vault password to temp file
|
||||||
|
with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmp:
|
||||||
|
tmp.write(vault_password)
|
||||||
|
tmp.flush()
|
||||||
|
vault_file = tmp.name
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"ansible-playbook",
|
||||||
|
"-i", inventory,
|
||||||
|
str(playbook_file),
|
||||||
|
"--vault-password-file", vault_file,
|
||||||
|
]
|
||||||
|
if limit != "all":
|
||||||
|
cmd.extend(["--limit", ",".join(limit)])
|
||||||
|
if roles:
|
||||||
|
cmd.extend(["--tags", ",".join(roles)])
|
||||||
|
print(cmd)
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, cwd=CLONE_DIR, check=True)
|
||||||
|
except:
|
||||||
|
print("not all jobs finished")
|
||||||
|
finally:
|
||||||
|
os.unlink(vault_file)
|
||||||
|
|
||||||
|
|
||||||
|
# ================= MAIN =================
|
||||||
|
def main():
|
||||||
|
print("Connecting to Vault...")
|
||||||
|
client = get_vault_client()
|
||||||
|
|
||||||
|
print("Retrieving secrets...")
|
||||||
|
gitlab_token = read_kv2_secret(client, VAULT_GITLAB_PATH, VAULT_GITLAB_FIELD)
|
||||||
|
ansible_vault_pass = read_kv2_secret(client, VAULT_ANSIBLE_PATH, VAULT_ANSIBLE_VAULT_FIELD)
|
||||||
|
ansible_ssh_key = read_kv2_secret(client, VAULT_ANSIBLE_PATH, VAULT_ANSIBLE_SSH_FIELD)
|
||||||
|
clone_or_update_repo(GITLAB_URL, GITLAB_BRANCH, gitlab_token)
|
||||||
|
inventory = select_inventory()
|
||||||
|
roles = select_roles() # purely informational
|
||||||
|
limit = select_limit()
|
||||||
|
|
||||||
|
print(f"Selected roles: {roles}")
|
||||||
|
print(f"Running playbook all.yml with inventory {inventory} and limit {limit}")
|
||||||
|
|
||||||
|
run_ansible_playbook(inventory, limit, roles, ansible_vault_pass, ansible_ssh_key)
|
||||||
|
print("Execution finished successfully.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user