snap-slack.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. #!/usr/bin/env python3
  2. import os
  3. import subprocess
  4. import toml
  5. import argparse
  6. from datetime import datetime
  7. # Path to the configuration file
  8. CONFIG_PATH = "/etc/snap-slack/config.toml"
  9. # Read configuration from a TOML file
  10. def read_config(file_path=CONFIG_PATH):
  11. if not os.path.exists(file_path):
  12. raise FileNotFoundError(f"Configuration file not found: {file_path}")
  13. with open(file_path, 'r') as f:
  14. config = toml.load(f)
  15. return config
  16. # Create a new snapshot with the current date and time
  17. def create(config):
  18. date_str = datetime.now().strftime("%Y%m%d-%H%M%S")
  19. snapshot_name = f"{config['snapshot']['snapshot_prefix']}{date_str}"
  20. snapshot_path = os.path.join(config['snapshot']['snapshot_dir'], snapshot_name)
  21. # Run the Btrfs snapshot command
  22. cmd = ["btrfs", "subvolume", "snapshot",
  23. os.path.join(config['snapshot']['btrfs_mount_point'], config['snapshot']['root_subvolume']),
  24. snapshot_path]
  25. result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  26. if result.returncode == 0:
  27. print(f"Created snapshot: {snapshot_name}")
  28. add_elilo_entry(config, snapshot_name)
  29. return snapshot_name
  30. else:
  31. raise Exception(f"Failed to create snapshot: {result.stderr.decode()}")
  32. # Add an entry for the snapshot in elilo.conf
  33. def add_elilo_entry(config, snapshot):
  34. # Get the extra boot options from the config, if any
  35. extra_boot_options = config['elilo'].get('extra_boot_options', '')
  36. # Construct the boot entry with optional extra boot options
  37. entry = f"""
  38. image=vmlinuz
  39. label={snapshot}
  40. root={config['elilo']['root_partition']}
  41. append="rootflags=subvol={config['snapshot']['snapshot_dir']}/{snapshot} ro {extra_boot_options}"
  42. read-only
  43. initrd=initrd.gz
  44. """
  45. # Append to elilo.conf if the entry doesn't already exist
  46. elilo_conf = config['elilo']['elilo_conf']
  47. with open(elilo_conf, 'r+') as f:
  48. content = f.read()
  49. if snapshot not in content:
  50. print(f"Adding entry for snapshot: {snapshot}")
  51. f.write(entry)
  52. # Remove an entry for a deleted snapshot from elilo.conf
  53. def remove_elilo_entry(config, snapshot):
  54. elilo_conf = config['elilo']['elilo_conf']
  55. with open(elilo_conf, 'r') as f:
  56. lines = f.readlines()
  57. with open(elilo_conf, 'w') as f:
  58. for line in lines:
  59. if snapshot not in line:
  60. f.write(line)
  61. print(f"Removed entry for snapshot: {snapshot}")
  62. # List all current snapshots
  63. def list_snapshots(config):
  64. snapshots = []
  65. for entry in os.listdir(config['snapshot']['snapshot_dir']):
  66. entry_path = os.path.join(config['snapshot']['snapshot_dir'], entry)
  67. if os.path.isdir(entry_path) and entry.startswith(config['snapshot']['snapshot_prefix']):
  68. snapshots.append(entry)
  69. return snapshots
  70. # Remove snapshots older than the retention period
  71. def remove_old_snapshots(config):
  72. retention_days = config['snapshot']['retention_days']
  73. now = datetime.now()
  74. for snapshot in list_snapshots(config):
  75. snapshot_path = os.path.join(config['snapshot']['snapshot_dir'], snapshot)
  76. created_time = datetime.fromtimestamp(os.path.getctime(snapshot_path))
  77. if (now - created_time).days > retention_days:
  78. # Remove snapshot
  79. cmd = ["btrfs", "subvolume", "delete", snapshot_path]
  80. subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  81. remove_elilo_entry(config, snapshot)
  82. print(f"Removed snapshot: {snapshot}")
  83. # Adopt a snapshot as the new root subvolume
  84. def adopt(config, snapshot):
  85. current_root_path = os.path.join(config['snapshot']['btrfs_mount_point'], config['snapshot']['root_subvolume'])
  86. snapshot_path = os.path.join(config['snapshot']['snapshot_dir'], snapshot)
  87. if not os.path.exists(snapshot_path):
  88. raise Exception(f"Snapshot '{snapshot}' not found.")
  89. # Step 1: Rename the current root subvolume to something else (e.g., @old_root)
  90. old_root_path = current_root_path + "_old"
  91. print(f"Renaming current root subvolume to {old_root_path}")
  92. cmd_rename_root = ["btrfs", "subvolume", "snapshot", current_root_path, old_root_path]
  93. subprocess.run(cmd_rename_root, check=True)
  94. # Step 2: Rename the selected snapshot to become the new root subvolume
  95. print(f"Renaming snapshot '{snapshot}' to become the new root subvolume.")
  96. cmd_set_new_root = ["btrfs", "subvolume", "snapshot", snapshot_path, current_root_path]
  97. subprocess.run(cmd_set_new_root, check=True)
  98. # Step 3: Update the elilo bootloader configuration to point to the new root subvolume
  99. print(f"Updating elilo.conf to use the new root subvolume '{snapshot}'.")
  100. add_elilo_entry(config, snapshot)
  101. print(f"Snapshot '{snapshot}' has been adopted as the new root subvolume.")
  102. # Main logic for managing snapshots
  103. def update(config):
  104. # Step 1: Get list of current snapshots and add them to elilo.conf if needed
  105. snapshots = list_snapshots(config)
  106. for snapshot in snapshots:
  107. add_elilo_entry(config, snapshot)
  108. # Step 2: Remove snapshots older than retention period
  109. remove_old_snapshots(config)
  110. # Parse command-line arguments
  111. def parse_arguments():
  112. parser = argparse.ArgumentParser(description='Manage BTRFS snapshots and elilo bootloader entries.')
  113. parser.add_argument('action', choices=['create', 'update', 'adopt'],
  114. help='Specify the action to perform: create, manage, or adopt.')
  115. parser.add_argument('--snapshot', help='The snapshot to adopt as the new root subvolume.')
  116. return parser.parse_args()
  117. def main():
  118. # Load configuration from /etc/manage_snapshots/config.toml
  119. config = read_config()
  120. # Parse command-line arguments
  121. args = parse_arguments()
  122. # Execute based on action
  123. if args.action == 'create':
  124. create(config)
  125. elif args.action == 'update':
  126. update(config)
  127. elif args.action == 'adopt':
  128. if not args.snapshot:
  129. print("Error: You must specify the snapshot to adopt using --snapshot.")
  130. else:
  131. adopt(config, args.snapshot)
  132. else:
  133. print("Invalid action")
  134. if __name__ == "__main__":
  135. main()