mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-29 15:06:26 +02:00
Refactor systemctl services and timers
- Unified service templates into generic systemctl templates - Introduced reusable filter plugins for script path handling - Updated path variables and service/timer definitions - Migrated roles (backup, cleanup, repair, etc.) to use systemctl role - Added sys-daemon role for core systemd cleanup - Simplified timer handling via sys-timer role Note: This is a large refactor and some errors may still exist. Further testing and adjustments will be needed.
This commit is contained in:
246
roles/update-docker/templates/script.py.j2
Normal file
246
roles/update-docker/templates/script.py.j2
Normal file
@@ -0,0 +1,246 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
def run_command(command):
|
||||
"""
|
||||
Executes the specified shell command, streaming and collecting its output in real-time.
|
||||
If the command exits with a non-zero status, a subprocess.CalledProcessError is raised,
|
||||
including the exit code, the executed command, and the full output (as bytes) for debugging purposes.
|
||||
"""
|
||||
process = None
|
||||
try:
|
||||
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
output = []
|
||||
|
||||
for line in iter(process.stdout.readline, b''):
|
||||
decoded_line = line.decode()
|
||||
output.append(decoded_line)
|
||||
sys.stdout.write(decoded_line)
|
||||
|
||||
return_code = process.wait()
|
||||
if return_code:
|
||||
full_output = ''.join(output)
|
||||
raise subprocess.CalledProcessError(return_code, command, output=full_output.encode())
|
||||
finally:
|
||||
if process and process.stdout:
|
||||
process.stdout.close()
|
||||
|
||||
def git_pull():
|
||||
"""
|
||||
Checks whether the Git repository in the specified directory is up to date and performs a git pull if necessary.
|
||||
|
||||
Raises:
|
||||
Exception: If retrieving the local or remote git revision fails because the command returns a non-zero exit code.
|
||||
"""
|
||||
print("Checking if the git repository is up to date.")
|
||||
|
||||
# Run 'git rev-parse @' and check its exit code explicitly.
|
||||
local_proc = subprocess.run("git rev-parse @", shell=True, capture_output=True)
|
||||
if local_proc.returncode != 0:
|
||||
error_msg = local_proc.stderr.decode().strip() or "Unknown error while retrieving local revision."
|
||||
raise Exception(f"Failed to retrieve local git revision: {error_msg}")
|
||||
local = local_proc.stdout.decode().strip()
|
||||
|
||||
# Run 'git rev-parse @{u}' and check its exit code explicitly.
|
||||
remote_proc = subprocess.run("git rev-parse @{u}", shell=True, capture_output=True)
|
||||
if remote_proc.returncode != 0:
|
||||
error_msg = remote_proc.stderr.decode().strip() or "Unknown error while retrieving remote revision."
|
||||
raise Exception(f"Failed to retrieve remote git revision: {error_msg}")
|
||||
remote = remote_proc.stdout.decode().strip()
|
||||
|
||||
if local != remote:
|
||||
print("Repository is not up to date. Performing git pull.")
|
||||
run_command("git pull")
|
||||
return True
|
||||
|
||||
print("Repository is already up to date.")
|
||||
return False
|
||||
|
||||
{% raw %}
|
||||
def get_image_digests(directory):
|
||||
"""
|
||||
Retrieves the image digests for all images in the specified Docker Compose project.
|
||||
"""
|
||||
compose_project = os.path.basename(directory)
|
||||
try:
|
||||
images_output = subprocess.check_output(
|
||||
f'docker images --format "{{{{.Repository}}}}:{{{{.Tag}}}}@{{{{.Digest}}}}" | grep {compose_project}',
|
||||
shell=True
|
||||
).decode().strip()
|
||||
return dict(line.split('@') for line in images_output.splitlines() if line)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode == 1: # grep no match found
|
||||
return {}
|
||||
else:
|
||||
raise # Other errors are still raised
|
||||
{% endraw %}
|
||||
|
||||
def is_any_service_up():
|
||||
"""
|
||||
Checks if any Docker services are currently running.
|
||||
"""
|
||||
process = subprocess.Popen("docker-compose ps -q", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
output, _ = process.communicate()
|
||||
service_ids = output.decode().strip().splitlines()
|
||||
return bool(service_ids)
|
||||
|
||||
def pull_docker_images():
|
||||
"""
|
||||
Pulls the latest Docker images for the project.
|
||||
"""
|
||||
print("Pulling docker images.")
|
||||
try:
|
||||
run_command("docker-compose pull")
|
||||
except subprocess.CalledProcessError as e:
|
||||
if "pull access denied" in e.output.decode() or "must be built from source" in e.output.decode():
|
||||
print("Need to build the image from source.")
|
||||
return True
|
||||
else:
|
||||
print("Failed to pull images with unexpected error.")
|
||||
raise
|
||||
return False
|
||||
|
||||
def update_docker(directory):
|
||||
"""
|
||||
Checks for updates to Docker images and rebuilds containers if necessary.
|
||||
"""
|
||||
print(f"Checking for updates to Docker images in {directory}.")
|
||||
before_digests = get_image_digests(directory)
|
||||
need_to_build = pull_docker_images()
|
||||
after_digests = get_image_digests(directory)
|
||||
if before_digests != after_digests:
|
||||
print("Changes detected in image digests. Rebuilding containers.")
|
||||
need_to_build = True
|
||||
|
||||
if need_to_build:
|
||||
# This propably just rebuilds the Dockerfile image if there is a change in the other docker compose containers
|
||||
run_command("docker-compose build --pull")
|
||||
start_docker(directory)
|
||||
else:
|
||||
print("Docker images are up to date. No rebuild necessary.")
|
||||
|
||||
def update_discourse(directory):
|
||||
"""
|
||||
Updates Discourse by running the rebuild command on the launcher script.
|
||||
"""
|
||||
docker_repository_directory = os.path.join(directory, "services", "{{ applications | get_app_conf('web-app-discourse','repository') }}")
|
||||
print(f"Using path {docker_repository_directory } to pull discourse repository.")
|
||||
os.chdir(docker_repository_directory )
|
||||
if git_pull():
|
||||
print("Start Discourse update procedure.")
|
||||
update_procedure("docker stop {{ applications | get_app_conf('web-app-discourse','docker.services.discourse.name') }}")
|
||||
update_procedure("docker rm {{ applications | get_app_conf('web-app-discourse','docker.services.discourse.name') }}")
|
||||
try:
|
||||
update_procedure("docker network connect {{ applications | get_app_conf('web-app-discourse','docker.network') }} {{ applications | get_app_conf('svc-db-postgres', 'docker.network') }}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_message = e.output.decode()
|
||||
if "already exists" in error_message or "is already connected" in error_message:
|
||||
print("Network connection already exists. Skipping...")
|
||||
else:
|
||||
raise
|
||||
update_procedure("./launcher rebuild {{ applications | get_app_conf('web-app-discourse','docker.services.discourse.name') }}")
|
||||
else:
|
||||
print("Discourse update skipped. No changes in git repository.")
|
||||
|
||||
def update_mastodon():
|
||||
"""
|
||||
Runs the database migration for Mastodon to ensure all required tables are up to date.
|
||||
"""
|
||||
print("Starting Mastodon database migration.")
|
||||
run_command("docker compose exec -T web bash -c 'RAILS_ENV={{ ENVIRONMENT | lower }} bin/rails db:migrate'")
|
||||
print("Mastodon database migration complete.")
|
||||
|
||||
def upgrade_listmonk():
|
||||
"""
|
||||
Runs the upgrade for Listmonk
|
||||
"""
|
||||
print("Starting Listmonk upgrade.")
|
||||
run_command('echo "y" | docker compose run -T application ./listmonk --upgrade')
|
||||
print("Upgrade complete.")
|
||||
|
||||
def update_nextcloud():
|
||||
"""
|
||||
Performs the necessary Nextcloud update procedures, including maintenance and app updates.
|
||||
"""
|
||||
print("Start Nextcloud upgrade procedure.")
|
||||
update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ upgrade")
|
||||
print("Start Nextcloud repairing procedure.")
|
||||
update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ maintenance:repair --include-expensive")
|
||||
print("Start Nextcloud update procedure.")
|
||||
update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ app:update --all")
|
||||
print("Start Nextcloud add-missing procedure.")
|
||||
update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ db:add-missing-columns")
|
||||
update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ db:add-missing-indices")
|
||||
update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ db:add-missing-primary-keys")
|
||||
print("Deactivate Maintanance Mode")
|
||||
update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ maintenance:mode --off")
|
||||
|
||||
def update_procedure(command):
|
||||
"""
|
||||
Attempts to execute a command up to a maximum number of retries.
|
||||
"""
|
||||
max_attempts = 3
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
run_command(command)
|
||||
break # If the command succeeds, exit the loop
|
||||
except subprocess.CalledProcessError as e:
|
||||
if attempt < max_attempts - 1: # Check if it's not the last attempt
|
||||
print(f"Attempt {attempt + 1} failed, retrying in 60 seconds...")
|
||||
time.sleep(60) # Wait for 60 seconds before retrying
|
||||
else:
|
||||
print("All attempts to update have failed.")
|
||||
raise # Re-raise the last exception after all attempts fail
|
||||
|
||||
def start_docker(directory):
|
||||
"""
|
||||
Starts or restarts Docker services in the specified directory.
|
||||
"""
|
||||
if is_any_service_up():
|
||||
print(f"Restarting containers in {directory}.")
|
||||
run_command("docker-compose up -d --force-recreate")
|
||||
else:
|
||||
print(f"Skipped starting. No service is up in {directory}.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Please provide the path to the parent directory as a parameter.")
|
||||
sys.exit(1)
|
||||
|
||||
parent_directory = sys.argv[1]
|
||||
for dir_entry in os.scandir(parent_directory):
|
||||
if dir_entry.is_dir():
|
||||
dir_path = dir_entry.path
|
||||
print(f"Checking for updates in: {dir_path}")
|
||||
os.chdir(dir_path)
|
||||
|
||||
# Pull git repository if it exist
|
||||
# @deprecated: This function should be removed in the future, as soon as all docker applications use the correct folder path
|
||||
if os.path.isdir(os.path.join(dir_path, ".git")):
|
||||
print("DEPRECATED: Docker .git repositories should be saved under /opt/docker/{instance}/services/{repository_name} ")
|
||||
git_pull()
|
||||
|
||||
if os.path.basename(dir_path) == "matrix":
|
||||
# No autoupdate for matrix is possible atm,
|
||||
# due to the reason that the role has to be executed every time.
|
||||
# The update has to be executed in the role
|
||||
# @todo implement in future
|
||||
pass
|
||||
else:
|
||||
# Pull and update docker images
|
||||
update_docker(dir_path)
|
||||
|
||||
# The following instances need additional update and upgrade procedures
|
||||
if os.path.basename(dir_path) == "discourse":
|
||||
update_discourse(dir_path)
|
||||
elif os.path.basename(dir_path) == "listmonk":
|
||||
upgrade_listmonk()
|
||||
elif os.path.basename(dir_path) == "mastodon":
|
||||
update_mastodon()
|
||||
elif os.path.basename(dir_path) == "nextcloud":
|
||||
update_nextcloud()
|
||||
|
||||
# @todo implement dedicated procedure for bluesky
|
||||
# @todo implement dedicated procedure for taiga
|
Reference in New Issue
Block a user