vps/manage.py

215 lines
6.4 KiB
Python
Raw Normal View History

#!/usr/bin/python3
import os, sys, re
import filecmp
from glob import glob
from mako.template import Template
import subprocess
import yaml
def backupProj(project):
print(f"Running backup for project {project}.")
# loop env.volumes & secrets.postgres
def getImageId (image):
return subprocess.run(["podman", "image", "inspect", "--format", "{{.Id}}", image], capture_output=True, text=True).stdout.strip()
def getUid(service):
if service in env['users'].keys():
user = env['users'][service] + env['uid_shift']
else:
2024-10-01 13:57:43 +02:00
user = 1000
return user
def pullProj(project):
print(f"Pulling images for project {project}.")
with open(f"projects/{project}/compose.yaml.rendered", 'r') as composefile:
images = re.findall('(?<=image:\\s).+', composefile.read())
pulledImages = []
for image in images:
currentId = getImageId(image)
subprocess.run(["podman", "pull", image])
pulledId = getImageId(image)
if currentId != pulledId:
pulledImages += image
print(f"Pulled new version of image {image}.")
else:
print(f"No update available for image {image}.")
return pulledImages
def renderFile(templateFile):
print(f"Rendering file {templateFile}.")
renderedFilename = re.sub('\\.mako$', '.rendered', templateFile)
template = Template(filename=templateFile)
outputFile = open(renderedFilename, "w")
outputFile.write(template.render(env=env, secrets=secrets))
outputFile.close()
def setCertPerms(service):
for path in ["/etc/letsencrypt", "/etc/letsencrypt/live", "/etc/letsencrypt/archive"]:
setOwner(path, 0, 0)
setPerms(path, 751)
pkeyFile = env['certs'][service]['pkey']
domain_dir = re.search('.+(?=\\/.+$)', pkeyFile).group(0)
for path in [domain_dir, re.sub('live', 'archive', domain_dir)]:
setOwner(path, env['host_uid'], getUid(service))
setPerms(path, 550)
setOwner(pkeyFile, 0, getUid(service))
setPerms(pkeyFile, 640)
def setNftables():
renderFile("nftables.conf.mako")
if not filecmp.cmp("nftables.conf.rendered", "/etc/nftables.conf"):
print("nftables.conf changed, copying new version.")
sudoRun(["cp", "nftables.conf.rendered", "/etc/nftables.conf"])
sudoRun(["systemctl", "restart", "nftables"])
else:
print("nftables.conf unchanged.")
def setOwner(path, uid=None, gid=None):
stat = os.stat(path)
2024-10-01 13:57:43 +02:00
if uid is None:
uid = stat.st_uid
2024-10-01 13:57:43 +02:00
if gid is None:
gid = stat.st_gid
if stat.st_uid != uid or stat.st_gid != gid:
print(f"Changing ownership of {path} to {uid}:{gid} from {stat.st_uid}:{stat.st_gid}.")
sudoRun(["chown", f"{uid}:{gid}", path])
else:
print(f"Ownership of {path} already set to {uid}:{gid}.")
def setPerms(path, mode):
mode = str(mode)
stat = os.stat(path)
curMode = oct(stat.st_mode)[-3:]
if mode != curMode:
print(f"Changing permissions of {path} to {mode} from {curMode}.")
if stat.st_uid == env['host_uid']:
subprocess.run(["chmod", mode, path])
else:
subprocess.run(["sudo", "chmod", mode, path])
else:
print(f"Permissions of {path} already set to {mode}.")
def setupProj(project):
print(f"Running setup for project {project}.")
backupProj(project)
if project in env['certs'].keys():
setCertPerms(project)
for templateFile in glob(f"projects/{project}/*.mako", include_hidden=True):
renderFile(templateFile)
renderedFilename = re.sub('\\.mako$', '.rendered', templateFile)
setPerms(renderedFilename, 640)
setOwner(renderedFilename, env['host_uid'], getUid(project))
upProj(project)
def sudoRun(args):
child = subprocess.Popen(["sudo"] + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if re.search('^\\[sudo\\] password for .+', child.stdout.read().strip()):
child.communicate(input=input().encode())[0]
def upProj(project):
print(f"Creating & starting stack for project {project}.")
if subprocess.run(["podman", "container", "exists", project]).returncode == 0:
subprocess.run(["podman-compose", "-f", f"projects/{project}/compose.yaml.rendered", "down"])
subprocess.run(["podman-compose", "-f", f"projects/{project}/compose.yaml.rendered", "up", "-d"])
def updateProj(project):
if not os.path.isfile(f"projects/{project}/compose.yaml.rendered"):
setupProj(project)
print(f"Running update for project {project}.")
if len(pullProj(project)) > 0:
backupProj(project)
upProj(project)
def main():
envFile = "pyenv.yml"
secretsFile = "pysecrets.yml"
os.chdir(os.path.realpath(sys.path[0]))
with open(envFile, 'r') as envfile, open(secretsFile, 'r') as secretsfile:
global env, secrets
env = yaml.safe_load(envfile)
env = yaml.safe_load(Template(filename=envFile).render(env=env))
secrets = yaml.safe_load(secretsfile)
setOwner(secretsFile, env['host_uid'], env['host_uid'])
setPerms(secretsFile, 600)
print("\nChoose action:")
print("[1/S] Setup project")
print("[2/U] Update project")
print("[3/B] Backup project")
action = ''
while action == '':
action = input("Action: ")
projects = os.listdir("projects")
print(f"\nProjects list: {projects}")
target_projects = input("Target compose project(s), space separated, leave empty to target all: ")
if target_projects == '':
target_projects = projects
else:
target_projects = target_projects.split(' ')
print(f"Target projects: {target_projects}")
setNftables()
match action:
case '1' | 'S':
for project in target_projects:
try:
print()
setupProj(project)
except Exception as e:
print(e, file=sys.stderr)
print(f"Failed to setup project {project}.", file=sys.stderr)
case '2' | 'U':
for project in target_projects:
try:
print()
updateProj(project)
except Exception as e:
print(e, file=sys.stderr)
print(f"Failed to update project {project}.", file=sys.stderr)
if __name__ == "__main__":
main()