#!/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: 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) if uid is None: uid = stat.st_uid 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()