#!/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 runPodman("image", ["inspect", "--format", "{{.Id}}", image]).stdout.strip() def getUid(service): if service in env['users'].keys(): return env['users'][service] + env['uid_shift'] else: return env['podman_uid'] 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) runPodman("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 runPodman(cmd, args): if isinstance(args, str): args = args.split(' ') if cmd == 'compose': runArgs = ["podman-compose"] + args else: runArgs = ["podman", cmd] + args if env['rootless']: return subprocess.run(runArgs, capture_output=True, text=True) else: return runSudo(runArgs) def runSudo(args): if isinstance(args, str): args = args.split(' ') 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] child.wait() stderr = child.stderr.read().strip() if stderr != '': print(stderr, file=sys.stderr) return child def setCertPerms(service): dirs = ["/etc/letsencrypt", "/etc/letsencrypt/live", "/etc/letsencrypt/archive"] pkeyFile = env['certs'][service]['pkey'] domain_dir = re.search('.+(?=\\/.+$)', pkeyFile).group(0) dirs += [domain_dir, re.sub('live', 'archive', domain_dir)] for path in dirs: setOwner(path, 0, env['podman_uid']) setPerms(path, 711) 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.") runSudo(["cp", "nftables.conf.rendered", "/etc/nftables.conf"]) runSudo(["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}.") runSudo(["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 == os.getuid(): subprocess.run(["chmod", mode, path]) else: runSudo(["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 == 'diun': if not os.path.isfile(env['socket']): if env['rootless']: subprocess.run(["systemctl", "--user", "restart", "podman.socket"]) else: runSudo("systemctl restart podman.socket") setPerms(env['socket'], 660) setOwner(env['socket'], env['podman_uid'], getUid('diun')) 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, os.getuid(), getUid(project)) upProj(project) def upProj(project): if runPodman("container", ["exists", project]).returncode == 0: print(f"Tearing down stack for project {project}.") runPodman("compose", ["-f", f"projects/{project}/compose.yaml.rendered", "down"]) print(f"Creating & starting stack for project {project}.") runPodman("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(Template(filename=envFile).render()) secrets = yaml.safe_load(secretsfile) setOwner(secretsFile, os.getuid(), os.getgid()) setPerms(secretsFile, 600) print("\nUsing socket " + env['socket'] + ".") 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()