Linux lhjmq-records 5.15.0-118-generic #128-Ubuntu SMP Fri Jul 5 09:28:59 UTC 2024 x86_64
Your IP : 3.21.12.122
# Copyright (C) 2011 Canonical
#
# Author:
# Michael Vogt
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; version 3.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from __future__ import print_function, unicode_literals
import datetime
import os
import subprocess
import sys
import time
import tempfile
from distutils.spawn import find_executable
from gettext import gettext as _
class AptBtrfsSnapshotError(Exception):
pass
class AptBtrfsNotSupportedError(AptBtrfsSnapshotError):
pass
class AptBtrfsRootWithNoatimeError(AptBtrfsSnapshotError):
pass
class FstabEntry(object):
""" a single fstab entry line """
@classmethod
def from_line(cls, line):
# split up
args = line.partition("#")[0].split()
# use only the first 7 args and ignore anything after them, mount
# seems to do the same, see bug #873411 comment #7
return FstabEntry(*args[0:6])
def __init__(self, fs_spec, mountpoint, fstype, options, dump=0, passno=0):
# uuid or device
self.fs_spec = fs_spec
self.mountpoint = mountpoint
self.fstype = fstype
self.options = options
self.dump = dump
self.passno = passno
def __repr__(self):
return "<FstabEntry '%s' '%s' '%s' '%s' '%s' '%s'>" % (
self.fs_spec, self.mountpoint, self.fstype,
self.options, self.dump, self.passno)
class Fstab(list):
""" a list of FstabEntry items """
def __init__(self, fstab="/etc/fstab"):
super(Fstab, self).__init__()
with open(fstab) as fstab_file:
for stripped_line in (line.strip() for line in fstab_file):
if stripped_line == "" or stripped_line.startswith("#"):
continue
try:
entry = FstabEntry.from_line(stripped_line)
except ValueError:
continue
self.append(entry)
class LowLevelCommands(object):
""" lowlevel commands invoked to perform various tasks like
interact with mount and btrfs tools
"""
def mount(self, fs_spec, mountpoint):
ret = subprocess.call(["mount", fs_spec, mountpoint])
return ret == 0
def umount(self, mountpoint):
ret = subprocess.call(["umount", mountpoint])
return ret == 0
def btrfs_subvolume_snapshot(self, source, dest):
ret = subprocess.call(["btrfs", "subvolume", "snapshot",
source, dest])
return ret == 0
def btrfs_delete_snapshot(self, snapshot):
ret = subprocess.call(["btrfs", "subvolume", "delete", snapshot])
return ret == 0
class AptBtrfsSnapshot(object):
""" the high level object that interacts with the snapshot system """
# normal snapshot
SNAP_PREFIX = "@apt-snapshot-"
# backname when changing
BACKUP_PREFIX = SNAP_PREFIX + "old-root-"
def __init__(self, fstab="/etc/fstab"):
self.fstab = Fstab(fstab)
self.commands = LowLevelCommands()
self._btrfs_root_mountpoint = None
def snapshots_supported(self):
""" verify that the system supports apt btrfs snapshots
by checking if the right fs layout is used etc
"""
if not (find_executable("btrfs")):
return False
# check the fstab
entry = self._get_supported_btrfs_root_fstab_entry()
return entry is not None
def _get_supported_btrfs_root_fstab_entry(self):
""" return the supported btrfs root FstabEntry or None """
for entry in self.fstab:
if (
entry.mountpoint == "/" and
entry.fstype == "btrfs" and
"subvol=@" in entry.options):
return entry
return None
def _uuid_for_mountpoint(self, mountpoint, fstab="/etc/fstab"):
""" return the device or UUID for the given mountpoint """
for entry in self.fstab:
if entry.mountpoint == mountpoint:
return entry.fs_spec
return None
def mount_btrfs_root_volume(self):
uuid = self._uuid_for_mountpoint("/")
mountpoint = tempfile.mkdtemp(prefix="apt-btrfs-snapshot-mp-")
if not self.commands.mount(uuid, mountpoint):
return None
self._btrfs_root_mountpoint = mountpoint
return self._btrfs_root_mountpoint
def umount_btrfs_root_volume(self):
res = self.commands.umount(self._btrfs_root_mountpoint)
os.rmdir(self._btrfs_root_mountpoint)
self._btrfs_root_mountpoint = None
return res
def _get_now_str(self):
return datetime.datetime.now().replace(microsecond=0).isoformat(
str('_'))
def create_btrfs_root_snapshot(self, additional_prefix=""):
mp = self.mount_btrfs_root_volume()
snap_id = self._get_now_str()
source = os.path.join(mp, "@")
target = os.path.join(mp, self.SNAP_PREFIX + additional_prefix +
snap_id)
if os.path.exists(target):
print(_("INFO: snapshot directory '%s' already exists, "
"not creating duplicate") % (target,))
return True
else:
res = self.commands.btrfs_subvolume_snapshot(source, target)
self.umount_btrfs_root_volume()
return res
def get_btrfs_root_snapshots_list(self, older_than=0):
""" get the list of available snapshot
If "older_then" is given (in unixtime format) it will only include
snapshots that are older then the given date)
"""
result = []
# if older_than is used, ensure that the rootfs does not use
# "noatime"
if older_than != 0:
entry = self._get_supported_btrfs_root_fstab_entry()
if not entry:
raise AptBtrfsNotSupportedError()
if "noatime" in entry.options:
raise AptBtrfsRootWithNoatimeError()
# if there is no older than, interpret that as "now"
if older_than == 0:
older_than = time.time()
mp = self.mount_btrfs_root_volume()
for e in os.listdir(mp):
if e.startswith(self.SNAP_PREFIX):
# fstab is read when it was booted and when a snapshot is
# created (to check if there is support for btrfs)
atime = os.path.getatime(os.path.join(mp, e, "etc", "fstab"))
if atime < older_than:
result.append(e)
self.umount_btrfs_root_volume()
return result
def print_btrfs_root_snapshots(self):
print("Available snapshots:")
print(" \n".join(self.get_btrfs_root_snapshots_list()))
return True
def _parse_older_than_to_unixtime(self, timefmt):
now = time.time()
if not timefmt.endswith("d"):
raise Exception("Please specify time in days (e.g. 10d)")
days = int(timefmt[:-1])
return now - (days * 24 * 60 * 60)
def print_btrfs_root_snapshots_older_than(self, timefmt):
older_than_unixtime = self._parse_older_than_to_unixtime(timefmt)
try:
print("Available snapshots older than '%s':" % timefmt)
print(" \n".join(self.get_btrfs_root_snapshots_list(
older_than=older_than_unixtime)))
except AptBtrfsRootWithNoatimeError:
sys.stderr.write("Error: fstab option 'noatime' incompatible "
"with option")
return False
return True
def clean_btrfs_root_snapshots_older_than(self, timefmt):
res = True
older_than_unixtime = self._parse_older_than_to_unixtime(timefmt)
try:
for snap in self.get_btrfs_root_snapshots_list(
older_than=older_than_unixtime):
res &= self.delete_snapshot(snap)
except AptBtrfsRootWithNoatimeError:
sys.stderr.write("Error: fstab option 'noatime' incompatible with "
"option")
return False
return res
def command_set_default(self, snapshot_name):
res = self.set_default(snapshot_name)
return res
def set_default(self, snapshot_name, backup=True):
""" set new default """
mp = self.mount_btrfs_root_volume()
new_root = os.path.join(mp, snapshot_name)
if (
os.path.isdir(new_root) and
snapshot_name.startswith("@") and
snapshot_name != "@"):
default_root = os.path.join(mp, "@")
backup = os.path.join(mp, self.BACKUP_PREFIX + self._get_now_str())
os.rename(default_root, backup)
os.rename(new_root, default_root)
print("Default changed to %s, please reboot for changes to take "
"effect." % snapshot_name)
else:
print("You have selected an invalid snapshot. Please make sure "
"that it exists, and that it is not \"@\".")
self.umount_btrfs_root_volume()
return True
def delete_snapshot(self, snapshot_name):
mp = self.mount_btrfs_root_volume()
res = self.commands.btrfs_delete_snapshot(
os.path.join(mp, snapshot_name))
self.umount_btrfs_root_volume()
return res
|