~ubuntu-branches/ubuntu/quantal/ubuntu-release-upgrader/quantal

« back to all changes in this revision

Viewing changes to DistUpgrade/apt_btrfs_snapshot.py

  • Committer: Package Import Robot
  • Author(s): Brian Murray
  • Date: 2012-07-25 12:06:06 UTC
  • mfrom: (1.1.8 quantal)
  • Revision ID: package-import@ubuntu.com-20120725120606-qrrlcfucnmf3pmb8
Tags: 1:0.174
DistUpgrade/DistUpgradeApport.py: use a whitelist to ensure that only
specified files are gathered when creating an apport crash report
(LP: #1004503)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2011 Canonical
2
 
#
3
 
# Author:
4
 
#  Michael Vogt
5
 
#
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.
9
 
#
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
13
 
# details.
14
 
#
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
18
 
 
19
 
from __future__ import print_function, unicode_literals
20
 
 
21
 
import datetime
22
 
import os
23
 
import subprocess
24
 
import sys
25
 
import time
26
 
import tempfile
27
 
 
28
 
class AptBtrfsSnapshotError(Exception):
29
 
    pass
30
 
class AptBtrfsNotSupportedError(AptBtrfsSnapshotError):
31
 
    pass
32
 
class AptBtrfsRootWithNoatimeError(AptBtrfsSnapshotError):
33
 
    pass
34
 
 
35
 
class FstabEntry(object):
36
 
    """ a single fstab entry line """
37
 
    @classmethod
38
 
    def from_line(cls, line):
39
 
        # split up
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):
45
 
        # uuid or device
46
 
        self.fs_spec = fs_spec
47
 
        self.mountpoint = mountpoint
48
 
        self.fstype = fstype
49
 
        self.options = options
50
 
        self.dump = dump
51
 
        self.passno = passno
52
 
    def __repr__(self):
53
 
        return "<FstabEntry '%s' '%s' '%s' '%s' '%s' '%s'>" % (
54
 
            self.fs_spec, self.mountpoint, self.fstype,
55
 
            self.options, self.dump, self.passno)
56
 
 
57
 
class Fstab(list):
58
 
    """ a list of FstabEntry items """
59
 
    def __init__(self, fstab="/etc/fstab"):
60
 
        super(Fstab, self).__init__()
61
 
        
62
 
        with open(fstab) as fstab_file:
63
 
            for line in (l.strip() for l in fstab_file):
64
 
                if line == "" or line.startswith("#"):
65
 
                    continue
66
 
                try:
67
 
                    entry = FstabEntry.from_line(line)
68
 
                except ValueError:
69
 
                    continue
70
 
                self.append(entry)
71
 
 
72
 
class LowLevelCommands(object):
73
 
    """ lowlevel commands invoked to perform various tasks like
74
 
        interact with mount and btrfs tools
75
 
    """
76
 
    def mount(self, fs_spec, mountpoint):
77
 
        ret = subprocess.call(["mount", fs_spec, mountpoint])
78
 
        return ret == 0
79
 
    def umount(self, mountpoint):
80
 
        ret = subprocess.call(["umount", mountpoint])
81
 
        return ret == 0
82
 
    def btrfs_subvolume_snapshot(self, source, dest):
83
 
        ret = subprocess.call(["btrfs", "subvolume", "snapshot",
84
 
                               source, dest])
85
 
        return ret == 0
86
 
    def btrfs_delete_snapshot(self, snapshot):
87
 
        ret = subprocess.call(["btrfs", "subvolume", "delete", snapshot])
88
 
        return ret == 0
89
 
 
90
 
class AptBtrfsSnapshot(object):
91
 
    """ the high level object that interacts with the snapshot system """
92
 
    
93
 
    # normal snapshot
94
 
    SNAP_PREFIX = "@apt-snapshot-"
95
 
    # backname when changing
96
 
    BACKUP_PREFIX = SNAP_PREFIX+"old-root-"
97
 
    
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
105
 
        """
106
 
        # check for the helper binary
107
 
        if not os.path.exists("/sbin/btrfs"):
108
 
            return False
109
 
        # check the fstab
110
 
        entry = self._get_supported_btrfs_root_fstab_entry()
111
 
        return entry != None
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):
118
 
                return entry
119
 
        return None
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:
124
 
                return entry.fs_spec
125
 
        return None
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):
130
 
            return None
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
137
 
        return res
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()
147
 
        return res
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)
152
 
        """
153
 
        l = []
154
 
        # if older_than is used, ensure that the rootfs does not use
155
 
        # "noatime"
156
 
        if older_than != 0:
157
 
            entry = self._get_supported_btrfs_root_fstab_entry()
158
 
            if not entry:
159
 
                raise AptBtrfsNotSupportedError()
160
 
            if "noatime" in entry.options:
161
 
                raise AptBtrfsRootWithNoatimeError()
162
 
        # if there is no older than, interpret that as "now"
163
 
        if older_than == 0:
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:
172
 
                    l.append(e)
173
 
        self.umount_btrfs_root_volume()
174
 
        return l
175
 
    def print_btrfs_root_snapshots(self):
176
 
        print("Available snapshots:")
177
 
        print("  \n".join(self.get_btrfs_root_snapshots_list()))
178
 
        return True
179
 
    def _parse_older_than_to_unixtime(self, timefmt):
180
 
        now = time.time()
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)
187
 
        try:
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")
193
 
            return False
194
 
        return True
195
 
    def clean_btrfs_root_snapshots_older_than(self, timefmt):
196
 
        res = True
197
 
        older_than_unixtime = self._parse_older_than_to_unixtime(timefmt)
198
 
        try:
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")
204
 
            return False
205
 
        return res
206
 
    def command_set_default(self, snapshot_name):
207
 
        res = self.set_default(snapshot_name)
208
 
        print("Please reboot")
209
 
        return res
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()
219
 
        return True
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()
225
 
        return res