| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- #!/usr/bin/env python3
- import os
- import subprocess
- import toml
- import argparse
- from datetime import datetime
- # Path to the configuration file
- CONFIG_PATH = "/etc/snap-slack/config.toml"
- # Read configuration from a TOML file
- def read_config(file_path=CONFIG_PATH):
- if not os.path.exists(file_path):
- raise FileNotFoundError(f"Configuration file not found: {file_path}")
-
- with open(file_path, 'r') as f:
- config = toml.load(f)
- return config
- # Create a new snapshot with the current date and time
- def create(config):
- date_str = datetime.now().strftime("%Y%m%d-%H%M%S")
- snapshot_name = f"{config['snapshot']['snapshot_prefix']}{date_str}"
- snapshot_path = os.path.join(config['snapshot']['snapshot_dir'], snapshot_name)
- # Run the Btrfs snapshot command
- cmd = ["btrfs", "subvolume", "snapshot",
- os.path.join(config['snapshot']['btrfs_mount_point'], config['snapshot']['root_subvolume']),
- snapshot_path]
- result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- if result.returncode == 0:
- print(f"Created snapshot: {snapshot_name}")
- add_elilo_entry(config, snapshot_name)
- return snapshot_name
- else:
- raise Exception(f"Failed to create snapshot: {result.stderr.decode()}")
- # Add an entry for the snapshot in elilo.conf
- def add_elilo_entry(config, snapshot):
- # Get the extra boot options from the config, if any
- extra_boot_options = config['elilo'].get('extra_boot_options', '')
- # Construct the boot entry with optional extra boot options
- entry = f"""
- image=vmlinuz
- label={snapshot}
- root={config['elilo']['root_partition']}
- append="rootflags=subvol={config['snapshot']['snapshot_dir']}/{snapshot} ro {extra_boot_options}"
- read-only
- initrd=initrd.gz
- """
- # Append to elilo.conf if the entry doesn't already exist
- elilo_conf = config['elilo']['elilo_conf']
- with open(elilo_conf, 'r+') as f:
- content = f.read()
- if snapshot not in content:
- print(f"Adding entry for snapshot: {snapshot}")
- f.write(entry)
- # Remove an entry for a deleted snapshot from elilo.conf
- def remove_elilo_entry(config, snapshot):
- elilo_conf = config['elilo']['elilo_conf']
- with open(elilo_conf, 'r') as f:
- lines = f.readlines()
- with open(elilo_conf, 'w') as f:
- for line in lines:
- if snapshot not in line:
- f.write(line)
- print(f"Removed entry for snapshot: {snapshot}")
- # List all current snapshots
- def list_snapshots(config):
- snapshots = []
- for entry in os.listdir(config['snapshot']['snapshot_dir']):
- entry_path = os.path.join(config['snapshot']['snapshot_dir'], entry)
- if os.path.isdir(entry_path) and entry.startswith(config['snapshot']['snapshot_prefix']):
- snapshots.append(entry)
- return snapshots
- # Remove snapshots older than the retention period
- def remove_old_snapshots(config):
- retention_days = config['snapshot']['retention_days']
- now = datetime.now()
- for snapshot in list_snapshots(config):
- snapshot_path = os.path.join(config['snapshot']['snapshot_dir'], snapshot)
- created_time = datetime.fromtimestamp(os.path.getctime(snapshot_path))
- if (now - created_time).days > retention_days:
- # Remove snapshot
- cmd = ["btrfs", "subvolume", "delete", snapshot_path]
- subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- remove_elilo_entry(config, snapshot)
- print(f"Removed snapshot: {snapshot}")
- # Adopt a snapshot as the new root subvolume
- def adopt(config, snapshot):
- current_root_path = os.path.join(config['snapshot']['btrfs_mount_point'], config['snapshot']['root_subvolume'])
- snapshot_path = os.path.join(config['snapshot']['snapshot_dir'], snapshot)
- if not os.path.exists(snapshot_path):
- raise Exception(f"Snapshot '{snapshot}' not found.")
- # Step 1: Rename the current root subvolume to something else (e.g., @old_root)
- old_root_path = current_root_path + "_old"
- print(f"Renaming current root subvolume to {old_root_path}")
- cmd_rename_root = ["btrfs", "subvolume", "snapshot", current_root_path, old_root_path]
- subprocess.run(cmd_rename_root, check=True)
- # Step 2: Rename the selected snapshot to become the new root subvolume
- print(f"Renaming snapshot '{snapshot}' to become the new root subvolume.")
- cmd_set_new_root = ["btrfs", "subvolume", "snapshot", snapshot_path, current_root_path]
- subprocess.run(cmd_set_new_root, check=True)
- # Step 3: Update the elilo bootloader configuration to point to the new root subvolume
- print(f"Updating elilo.conf to use the new root subvolume '{snapshot}'.")
- add_elilo_entry(config, snapshot)
- print(f"Snapshot '{snapshot}' has been adopted as the new root subvolume.")
- # Main logic for managing snapshots
- def update(config):
- # Step 1: Get list of current snapshots and add them to elilo.conf if needed
- snapshots = list_snapshots(config)
- for snapshot in snapshots:
- add_elilo_entry(config, snapshot)
- # Step 2: Remove snapshots older than retention period
- remove_old_snapshots(config)
- # Parse command-line arguments
- def parse_arguments():
- parser = argparse.ArgumentParser(description='Manage BTRFS snapshots and elilo bootloader entries.')
- parser.add_argument('action', choices=['create', 'update', 'adopt'],
- help='Specify the action to perform: create, manage, or adopt.')
- parser.add_argument('--snapshot', help='The snapshot to adopt as the new root subvolume.')
- return parser.parse_args()
- def main():
- # Load configuration from /etc/manage_snapshots/config.toml
- config = read_config()
- # Parse command-line arguments
- args = parse_arguments()
- # Execute based on action
- if args.action == 'create':
- create(config)
- elif args.action == 'update':
- update(config)
- elif args.action == 'adopt':
- if not args.snapshot:
- print("Error: You must specify the snapshot to adopt using --snapshot.")
- else:
- adopt(config, args.snapshot)
- else:
- print("Invalid action")
- if __name__ == "__main__":
- main()
|