update_manager.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import os
  2. import subprocess
  3. import logging
  4. from typing import Dict, List, Optional, Tuple, Callable
  5. # Configure logging
  6. logger = logging.getLogger(__name__)
  7. def check_git_updates():
  8. """Check for available Git updates."""
  9. try:
  10. logger.debug("Checking for Git updates")
  11. subprocess.run(["git", "fetch", "--tags", "--force"], check=True)
  12. latest_remote_tag = subprocess.check_output(
  13. ["git", "describe", "--tags", "--abbrev=0", "origin/main"]
  14. ).strip().decode()
  15. latest_local_tag = subprocess.check_output(
  16. ["git", "describe", "--tags", "--abbrev=0"]
  17. ).strip().decode()
  18. tag_behind_count = 0
  19. if latest_local_tag != latest_remote_tag:
  20. tags = subprocess.check_output(
  21. ["git", "tag", "--merged", "origin/main"], text=True
  22. ).splitlines()
  23. found_local = False
  24. for tag in tags:
  25. if tag == latest_local_tag:
  26. found_local = True
  27. elif found_local:
  28. tag_behind_count += 1
  29. if tag == latest_remote_tag:
  30. break
  31. updates_available = latest_remote_tag != latest_local_tag
  32. logger.info(f"Updates available: {updates_available}, {tag_behind_count} versions behind")
  33. return {
  34. "updates_available": updates_available,
  35. "tag_behind_count": tag_behind_count,
  36. "latest_remote_tag": latest_remote_tag,
  37. "latest_local_tag": latest_local_tag,
  38. }
  39. except subprocess.CalledProcessError as e:
  40. logger.error(f"Error checking Git updates: {e}")
  41. return {
  42. "updates_available": False,
  43. "tag_behind_count": 0,
  44. "latest_remote_tag": None,
  45. "latest_local_tag": None,
  46. }
  47. def list_available_versions() -> Dict[str, List[str]]:
  48. """List all available Git tags and branches."""
  49. try:
  50. logger.debug("Fetching available versions")
  51. # Fetch latest from remote
  52. subprocess.run(["git", "fetch", "--all", "--tags", "--force"], check=True, capture_output=True)
  53. # Get all tags, sorted by version (newest first)
  54. tags_output = subprocess.check_output(
  55. ["git", "tag", "--sort=-version:refname"],
  56. text=True
  57. ).strip()
  58. tags = [tag for tag in tags_output.split('\n') if tag]
  59. # Get all remote branches
  60. branches_output = subprocess.check_output(
  61. ["git", "branch", "-r", "--format=%(refname:short)"],
  62. text=True
  63. ).strip()
  64. # Filter out HEAD and extract branch names
  65. branches = []
  66. for branch in branches_output.split('\n'):
  67. if branch and not branch.endswith('/HEAD'):
  68. # Remove 'origin/' prefix
  69. branch_name = branch.replace('origin/', '')
  70. if branch_name not in ['HEAD']:
  71. branches.append(branch_name)
  72. logger.info(f"Found {len(tags)} tags and {len(branches)} branches")
  73. return {
  74. "tags": tags,
  75. "branches": branches
  76. }
  77. except subprocess.CalledProcessError as e:
  78. logger.error(f"Error listing versions: {e}")
  79. return {
  80. "tags": [],
  81. "branches": []
  82. }
  83. def update_software(version: Optional[str] = None, log_callback: Optional[Callable[[str], None]] = None):
  84. """Update the software to the specified version or latest."""
  85. error_log = []
  86. def log(message: str):
  87. """Log message and call callback if provided."""
  88. logger.info(message)
  89. if log_callback:
  90. log_callback(message)
  91. log("Starting software update process")
  92. def run_command_with_output(command, description):
  93. """Run command and stream output to log callback."""
  94. try:
  95. log(f"Running: {description}")
  96. log(f"Command: {' '.join(command)}")
  97. # Run command and capture output in real-time
  98. process = subprocess.Popen(
  99. command,
  100. stdout=subprocess.PIPE,
  101. stderr=subprocess.STDOUT,
  102. text=True,
  103. bufsize=1
  104. )
  105. # Stream output line by line
  106. for line in iter(process.stdout.readline, ''):
  107. if line:
  108. log(line.rstrip())
  109. process.wait()
  110. if process.returncode != 0:
  111. error_msg = f"{description} failed with return code {process.returncode}"
  112. log(f"ERROR: {error_msg}")
  113. error_log.append(error_msg)
  114. return False
  115. log(f"✓ {description} completed successfully")
  116. return True
  117. except Exception as e:
  118. error_msg = f"{description} failed: {str(e)}"
  119. log(f"ERROR: {error_msg}")
  120. error_log.append(error_msg)
  121. return False
  122. # Determine target version
  123. try:
  124. log("Fetching latest version information...")
  125. subprocess.run(["git", "fetch", "--all", "--tags", "--force"], check=True, capture_output=True)
  126. if not version or version == "latest":
  127. # Get latest tag
  128. target_version = subprocess.check_output(
  129. ["git", "describe", "--tags", "--abbrev=0", "origin/main"]
  130. ).strip().decode()
  131. log(f"Target version: {target_version} (latest)")
  132. else:
  133. target_version = version
  134. log(f"Target version: {target_version} (user selected)")
  135. except subprocess.CalledProcessError as e:
  136. error_msg = f"Failed to fetch version information: {e}"
  137. log(f"ERROR: {error_msg}")
  138. error_log.append(error_msg)
  139. return False, error_msg, error_log
  140. # Pull Docker images
  141. if not run_command_with_output(
  142. ["docker", "compose", "pull"],
  143. "Pulling Docker images"
  144. ):
  145. return False, "Failed to pull Docker images", error_log
  146. # Checkout target version
  147. if not run_command_with_output(
  148. ["git", "checkout", target_version, "--force"],
  149. f"Checking out version {target_version}"
  150. ):
  151. return False, f"Failed to checkout version {target_version}", error_log
  152. # Restart Docker containers
  153. if not run_command_with_output(
  154. ["docker", "compose", "up", "-d", "--remove-orphans"],
  155. "Restarting Docker containers"
  156. ):
  157. return False, "Failed to restart Docker containers", error_log
  158. log("✓ Software update completed successfully!")
  159. log(f"System is now running version: {target_version}")
  160. return True, None, None