214 lines
6.4 KiB
Python
Executable file
214 lines
6.4 KiB
Python
Executable file
#!/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()
|
|
|