feat: add Proxmox provisioning service with CI and deployment tooling
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
case "${1:-}" in check|apply|verify) ;; *) exit 2 ;; esac
|
||||
exec python3 - "$1" "$2" <<'PY'
|
||||
import json, os, pathlib, re, subprocess, sys, tempfile, urllib.parse
|
||||
def has_repository_indexes(policy, url, suite, components):
|
||||
indexes = [line.split() for line in policy.splitlines()]
|
||||
return all(any(any(field.rstrip('/') == url.rstrip('/') for field in fields) and f'{suite}/{component}' in fields for fields in indexes) for component in components)
|
||||
|
||||
mode, params_file = sys.argv[1:]
|
||||
p = json.loads(pathlib.Path(params_file).read_text())
|
||||
url, suite, components, keyring = (p.get(k) for k in ('url', 'suite', 'components', 'keyring'))
|
||||
if not isinstance(url, str) or any(c.isspace() for c in url):
|
||||
raise SystemExit('An explicit HTTPS repository URL is required')
|
||||
parsed = urllib.parse.urlsplit(url)
|
||||
if parsed.scheme != 'https' or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment:
|
||||
raise SystemExit('Repository URL must use HTTPS without credentials or query parameters')
|
||||
if not isinstance(suite, str) or not re.fullmatch(r'[a-z][a-z0-9-]{0,40}', suite):
|
||||
raise SystemExit('Invalid repository suite')
|
||||
if not isinstance(components, list) or not components or any(not isinstance(c, str) or not re.fullmatch(r'[a-z][a-z0-9/-]{0,50}', c) for c in components):
|
||||
raise SystemExit('Invalid repository components')
|
||||
if not isinstance(keyring, str) or not re.fullmatch(r'/(usr/share|etc/apt)/keyrings/[A-Za-z0-9_.-]+\.(gpg|asc)', keyring) or not pathlib.Path(keyring).is_file():
|
||||
raise SystemExit('An existing administrator-provisioned APT keyring is required')
|
||||
expected = f'Types: deb\nURIs: {url}\nSuites: {suite}\nComponents: {" ".join(components)}\nSigned-By: {keyring}\n'
|
||||
target = pathlib.Path('/etc/apt/sources.list.d/pve-provisioner.sources')
|
||||
matches = target.is_file() and target.read_text() == expected
|
||||
if mode == 'check':
|
||||
sys.exit(0 if matches else 1)
|
||||
if mode == 'apply' and not matches:
|
||||
fd, name = tempfile.mkstemp(prefix='.pve-provisioner-', dir=target.parent)
|
||||
try:
|
||||
os.fchmod(fd, 0o644)
|
||||
with os.fdopen(fd, 'w') as stream:
|
||||
stream.write(expected)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(name, target)
|
||||
finally:
|
||||
if os.path.exists(name):
|
||||
os.unlink(name)
|
||||
if mode == 'apply':
|
||||
subprocess.run(['apt-get', '-o', 'DPkg::Lock::Timeout=180', 'update'], check=True, timeout=500)
|
||||
policy = subprocess.run(['apt-cache', 'policy'], check=True, capture_output=True, text=True, timeout=30).stdout
|
||||
indexed = has_repository_indexes(policy, url, suite, components)
|
||||
passed = target.is_file() and target.read_text() == expected and indexed
|
||||
print(json.dumps({'passed': passed, 'repository': url, 'suite': suite}))
|
||||
sys.exit(0 if passed else 1)
|
||||
PY
|
||||
Reference in New Issue
Block a user