1
# Copyright (C) 2011 Canonical
6
# This program is free software; you can redistribute it and/or modify it under
7
# the terms of the GNU General Public License as published by the Free Software
8
# Foundation; version 3.
10
# This program is distributed in the hope that it will be useful, but WITHOUT
11
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
15
# You should have received a copy of the GNU General Public License along with
16
# this program; if not, write to the Free Software Foundation, Inc.,
17
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
from __future__ import print_function, unicode_literals
28
class AptBtrfsSnapshotError(Exception):
30
class AptBtrfsNotSupportedError(AptBtrfsSnapshotError):
32
class AptBtrfsRootWithNoatimeError(AptBtrfsSnapshotError):
35
class FstabEntry(object):
36
""" a single fstab entry line """
38
def from_line(cls, line):
40
args = line.partition("#")[0].split()
41
# use only the first 7 args and ignore anything after them, mount
42
# seems to do the same, see bug #873411 comment #7
43
return FstabEntry(*args[0:6])
44
def __init__(self, fs_spec, mountpoint, fstype, options, dump=0, passno=0):
46
self.fs_spec = fs_spec
47
self.mountpoint = mountpoint
49
self.options = options
53
return "<FstabEntry '%s' '%s' '%s' '%s' '%s' '%s'>" % (
54
self.fs_spec, self.mountpoint, self.fstype,
55
self.options, self.dump, self.passno)
58
""" a list of FstabEntry items """
59
def __init__(self, fstab="/etc/fstab"):
60
super(Fstab, self).__init__()
62
with open(fstab) as fstab_file:
63
for line in (l.strip() for l in fstab_file):
64
if line == "" or line.startswith("#"):
67
entry = FstabEntry.from_line(line)
72
class LowLevelCommands(object):
73
""" lowlevel commands invoked to perform various tasks like
74
interact with mount and btrfs tools
76
def mount(self, fs_spec, mountpoint):
77
ret = subprocess.call(["mount", fs_spec, mountpoint])
79
def umount(self, mountpoint):
80
ret = subprocess.call(["umount", mountpoint])
82
def btrfs_subvolume_snapshot(self, source, dest):
83
ret = subprocess.call(["btrfs", "subvolume", "snapshot",
86
def btrfs_delete_snapshot(self, snapshot):
87
ret = subprocess.call(["btrfs", "subvolume", "delete", snapshot])
90
class AptBtrfsSnapshot(object):
91
""" the high level object that interacts with the snapshot system """
94
SNAP_PREFIX = "@apt-snapshot-"
95
# backname when changing
96
BACKUP_PREFIX = SNAP_PREFIX+"old-root-"
98
def __init__(self, fstab="/etc/fstab"):
99
self.fstab = Fstab(fstab)
100
self.commands = LowLevelCommands()
101
self._btrfs_root_mountpoint = None
102
def snapshots_supported(self):
103
""" verify that the system supports apt btrfs snapshots
104
by checking if the right fs layout is used etc
106
# check for the helper binary
107
if not os.path.exists("/sbin/btrfs"):
110
entry = self._get_supported_btrfs_root_fstab_entry()
112
def _get_supported_btrfs_root_fstab_entry(self):
113
""" return the supported btrfs root FstabEntry or None """
114
for entry in self.fstab:
115
if (entry.mountpoint == "/" and
116
entry.fstype == "btrfs" and
117
"subvol=@" in entry.options):
120
def _uuid_for_mountpoint(self, mountpoint, fstab="/etc/fstab"):
121
""" return the device or UUID for the given mountpoint """
122
for entry in self.fstab:
123
if entry.mountpoint == mountpoint:
126
def mount_btrfs_root_volume(self):
127
uuid = self._uuid_for_mountpoint("/")
128
mountpoint = tempfile.mkdtemp(prefix="apt-btrfs-snapshot-mp-")
129
if not self.commands.mount(uuid, mountpoint):
131
self._btrfs_root_mountpoint = mountpoint
132
return self._btrfs_root_mountpoint
133
def umount_btrfs_root_volume(self):
134
res = self.commands.umount(self._btrfs_root_mountpoint)
135
os.rmdir(self._btrfs_root_mountpoint)
136
self._btrfs_root_mountpoint = None
138
def _get_now_str(self):
139
return datetime.datetime.now().replace(microsecond=0).isoformat(str('_'))
140
def create_btrfs_root_snapshot(self, additional_prefix=""):
141
mp = self.mount_btrfs_root_volume()
142
snap_id = self._get_now_str()
143
res = self.commands.btrfs_subvolume_snapshot(
144
os.path.join(mp, "@"),
145
os.path.join(mp, self.SNAP_PREFIX+additional_prefix+snap_id))
146
self.umount_btrfs_root_volume()
148
def get_btrfs_root_snapshots_list(self, older_than=0):
149
""" get the list of available snapshot
150
If "older_then" is given (in unixtime format) it will only include
151
snapshots that are older then the given date)
154
# if older_than is used, ensure that the rootfs does not use
157
entry = self._get_supported_btrfs_root_fstab_entry()
159
raise AptBtrfsNotSupportedError()
160
if "noatime" in entry.options:
161
raise AptBtrfsRootWithNoatimeError()
162
# if there is no older than, interpret that as "now"
164
older_than = time.time()
165
mp = self.mount_btrfs_root_volume()
166
for e in os.listdir(mp):
167
if e.startswith(self.SNAP_PREFIX):
168
# fstab is read when it was booted and when a snapshot is
169
# created (to check if there is support for btrfs)
170
atime = os.path.getatime(os.path.join(mp, e, "etc", "fstab"))
171
if atime < older_than:
173
self.umount_btrfs_root_volume()
175
def print_btrfs_root_snapshots(self):
176
print("Available snapshots:")
177
print(" \n".join(self.get_btrfs_root_snapshots_list()))
179
def _parse_older_than_to_unixtime(self, timefmt):
181
if not timefmt.endswith("d"):
182
raise Exception("Please specify time in days (e.g. 10d)")
183
days = int(timefmt[:-1])
184
return now - (days * 24 * 60 * 60)
185
def print_btrfs_root_snapshots_older_than(self, timefmt):
186
older_than_unixtime = self._parse_older_than_to_unixtime(timefmt)
188
print("Available snapshots older than '%s':" % timefmt)
189
print(" \n".join(self.get_btrfs_root_snapshots_list(
190
older_than=older_than_unixtime)))
191
except AptBtrfsRootWithNoatimeError:
192
sys.stderr.write("Error: fstab option 'noatime' incompatible with option")
195
def clean_btrfs_root_snapshots_older_than(self, timefmt):
197
older_than_unixtime = self._parse_older_than_to_unixtime(timefmt)
199
for snap in self.get_btrfs_root_snapshots_list(
200
older_than=older_than_unixtime):
201
res &= self.delete_snapshot(snap)
202
except AptBtrfsRootWithNoatimeError:
203
sys.stderr.write("Error: fstab option 'noatime' incompatible with option")
206
def command_set_default(self, snapshot_name):
207
res = self.set_default(snapshot_name)
208
print("Please reboot")
210
def set_default(self, snapshot_name, backup=True):
211
""" set new default """
212
mp = self.mount_btrfs_root_volume()
213
new_root = os.path.join(mp, snapshot_name)
214
default_root = os.path.join(mp, "@")
215
backup = os.path.join(mp, self.BACKUP_PREFIX+self._get_now_str())
216
os.rename(default_root, backup)
217
os.rename(new_root, default_root)
218
self.umount_btrfs_root_volume()
220
def delete_snapshot(self, snapshot_name):
221
mp = self.mount_btrfs_root_volume()
222
res = self.commands.btrfs_delete_snapshot(
223
os.path.join(mp, snapshot_name))
224
self.umount_btrfs_root_volume()