computer-playbook/roles/update-docker/files/update-docker.py

190 lines
7.2 KiB
Python
Raw Normal View History

2023-11-16 12:31:12 +01:00
import os
import subprocess
import sys
2023-11-16 15:13:34 +01:00
import time
2023-11-16 12:31:12 +01:00
def run_command(command):
2023-11-16 15:42:09 +01:00
process = None
try:
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output = []
2023-11-16 14:24:13 +01:00
2023-11-16 15:42:09 +01:00
for line in iter(process.stdout.readline, b''):
decoded_line = line.decode()
output.append(decoded_line)
sys.stdout.write(decoded_line)
2023-11-16 14:24:13 +01:00
2023-11-16 15:42:09 +01:00
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()
2023-11-16 12:31:12 +01:00
2024-11-07 20:39:07 +01:00
def git_pull():
"""
Checks whether the Git repository in the specified directory is up to date and performs a git pull if necessary.
"""
2024-11-07 20:39:07 +01:00
print(f"Checking if the git repository is up to date.")
2023-11-16 12:31:12 +01:00
local = subprocess.check_output("git rev-parse @", shell=True).decode().strip()
remote = subprocess.check_output("git rev-parse @{u}", shell=True).decode().strip()
if local != remote:
print("Repository is not up to date. Performing git pull.")
run_command("git pull")
2024-11-08 03:50:46 +01:00
return True
print("Repository is already up to date.")
2024-11-08 03:50:46 +01:00
return False
2023-11-16 12:49:53 +01:00
def get_image_digests(directory):
2024-11-08 03:50:46 +01:00
"""
Retrieves the image digests for all images in the specified Docker Compose project.
"""
compose_project = os.path.basename(directory)
2023-11-16 14:42:09 +01:00
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
2023-11-16 12:31:12 +01:00
2024-11-07 20:39:07 +01:00
def is_any_service_up():
2024-11-08 03:50:46 +01:00
"""
Checks if any Docker services are currently running.
"""
2023-11-16 15:13:34 +01:00
process = subprocess.Popen("docker-compose ps -q", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output, _ = process.communicate()
service_ids = output.decode().strip().splitlines()
2024-11-08 03:50:46 +01:00
return bool(service_ids)
2023-11-16 15:13:34 +01:00
2024-11-07 20:39:07 +01:00
def pull_docker_images():
2024-11-08 03:50:46 +01:00
"""
Pulls the latest Docker images for the project.
"""
2023-11-16 12:49:53 +01:00
print("Pulling docker images.")
2023-11-16 14:04:42 +01:00
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.")
2024-11-07 20:39:07 +01:00
return True
2023-11-16 14:04:42 +01:00
else:
print("Failed to pull images with unexpected error.")
raise
2024-11-07 20:39:07 +01:00
return False
2023-11-16 12:49:53 +01:00
2024-11-07 20:39:07 +01:00
def update_docker(directory):
2024-11-08 03:50:46 +01:00
"""
Checks for updates to Docker images and rebuilds containers if necessary.
"""
2024-11-07 20:39:07 +01:00
print(f"Checking for updates to Docker images in {directory}.")
before_digests = get_image_digests(directory)
need_to_build = pull_docker_images()
2023-11-16 14:04:42 +01:00
after_digests = get_image_digests(directory)
2023-11-16 15:13:34 +01:00
if before_digests != after_digests:
2023-11-16 12:49:53 +01:00
print("Changes detected in image digests. Rebuilding containers.")
2024-11-08 03:50:46 +01:00
need_to_build = True
2023-11-16 14:33:06 +01:00
if need_to_build:
2023-11-16 15:13:34 +01:00
run_command("docker-compose build")
start_docker(directory)
2023-11-16 12:49:53 +01:00
else:
print("Docker images are up to date. No rebuild necessary.")
2023-11-16 12:31:12 +01:00
2024-11-08 03:50:46 +01:00
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=production bin/rails db:migrate'")
print("Mastodon database migration complete.")
def update_nextcloud():
2024-11-08 03:50:46 +01:00
"""
Performs the necessary Nextcloud update procedures, including maintenance and app updates.
"""
print("Start Nextcloud update procedure.")
2024-02-06 23:45:03 +01:00
update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ upgrade")
update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ maintenance:repair")
update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ app:update --all")
2024-02-06 23:45:03 +01:00
update_procedure("docker-compose exec -T -u www-data application /var/www/html/occ maintenance:mode --off")
def update_discourse(directory):
2024-11-08 03:50:46 +01:00
"""
Updates Discourse by running the rebuild command on the launcher script.
"""
os.chdir(directory)
print("Start Discourse update procedure.")
update_procedure("./launcher rebuild app")
2024-11-08 03:50:46 +01:00
def update_procedure(command):
2024-11-08 03:50:46 +01:00
"""
Attempts to execute a command up to a maximum number of retries.
"""
2023-11-16 15:13:34 +01:00
max_attempts = 3
for attempt in range(max_attempts):
try:
run_command(command)
2023-11-16 15:13:34 +01:00
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:
2024-11-08 03:50:46 +01:00
print("All attempts to update have failed.")
2023-11-16 15:13:34 +01:00
raise # Re-raise the last exception after all attempts fail
def start_docker(directory):
2024-11-08 03:50:46 +01:00
"""
Starts or restarts Docker services in the specified directory.
"""
2024-11-07 20:39:07 +01:00
if is_any_service_up():
2023-11-16 15:13:34 +01:00
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}.")
2023-11-16 12:31:12 +01:00
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}")
2024-11-07 20:39:07 +01:00
os.chdir(dir_path)
2023-11-16 12:31:12 +01:00
if os.path.isdir(os.path.join(dir_path, ".git")):
2024-11-07 20:39:07 +01:00
git_repository_was_pulled = git_pull()
2023-11-16 12:31:12 +01:00
# Discourse is an exception and uses own update command instead of docker compose
if os.path.basename(dir_path) == "discourse":
if git_repository_was_pulled:
update_discourse(dir_path)
else:
print("Discourse update skipped. No changes in git repository.")
2024-11-08 03:50:46 +01:00
elif 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:
2024-11-07 20:39:07 +01:00
# Pull and update docker images
update_docker(dir_path)
2024-11-07 20:39:07 +01:00
# Nextcloud needs additional update procedures
if os.path.basename(dir_path) == "nextcloud":
update_nextcloud()
2024-11-08 03:50:46 +01:00
elif os.path.basename(dir_path) == "mastodon":
update_mastodon()