210b25750a73fc5af99c8b40167ad0eeab8fa619
[cumulus.git] / python / cumulus / retention.py
1 # Cumulus: Efficient Filesystem Backup to the Cloud
2 # Copyright (C) 2012 The Cumulus Developers
3 # See the AUTHORS file for a list of contributors.
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19 """Backup retention policies.
20
21 Retention policies control how long different backup snapshots should be kept,
22 for example keeping daily snapshots for short periods of time but retaining
23 weekly snapshots going back further in time.
24 """
25
26 from __future__ import division, print_function, unicode_literals
27
28 import calendar
29 import datetime
30
31 TIMESTAMP_FORMAT = "%Y%m%dT%H%M%S"
32
33 # Different classes of backups--such as "daily" or "monthly"--can have
34 # different retention periods applied.  A single backup snapshot might belong
35 # to multiple classes (i.e., perhaps be both a "daily" and a "monthly", though
36 # not a "weekly").
37 #
38 # Backups are classified using partitioning functions, defined below.  For a
39 # "monthly" backup classifier, all backups for a given month should map to the
40 # same partition.  Then, we apply the class label to the earliest snapshot in
41 # each partition--so the set of "monthly" backups would consist of all backups
42 # which were the first to run after the start of a month.
43 #
44 # A partitioning function must take a datetime instance as input and return a
45 # partition representative as output; timestamps that should be part of the
46 # same partition should map to equal partition representatives.  For a
47 # "monthly" classifier, an easy way to do this is to truncate the timestamp to
48 # keep only the month and year, and in general truncating timestamps works
49 # well, but the values are not used in any other way than equality testing so
50 # any type is allowed.
51 #
52 # _backup_classes is a registry of useful backup types; it maps a descriptive
53 # name to a partition function which implements it.
54 _backup_classes = {}
55
56 def add_backup_class(name, partioning_function):
57     """Registers a new class of backups for which policies can be applied.
58
59     The new class will be available as name to RetentionEngine.add_policy.
60     partioning_function should be a function for grouping together backups in
61     the same time period.
62
63     Predefined backups classes are: "yearly", "monthly", "weekly", "daily", and
64     "all".
65     """
66     _backup_classes[name] = partioning_function
67
68 add_backup_class("yearly", lambda t: t.date().replace(day=1, month=1))
69 add_backup_class("monthly", lambda t: t.date().replace(day=1))
70 add_backup_class("weekly", lambda t: t.isocalendar()[0:2])
71 add_backup_class("daily", lambda t: t.date())
72 add_backup_class("all", lambda t: t)
73
74
75 class RetentionEngine(object):
76     """Class for applying a retention policy to a set of snapshots.
77
78     Allows a retention policy to be set, then matches a sequence of backup
79     snapshots to the policy to decide which ones should be kept.
80     """
81
82     def __init__(self):
83         self.set_utc(False)
84         self._policies = {}
85         self._last_snapshots = {}
86         self._now = datetime.datetime.utcnow()
87
88     def set_utc(self, use_utc=True):
89         """Perform policy matching with timestamps in UTC.
90
91         By default, the policy converts timestamps to local time, but calling
92         set_utc(True) will select snapshots based on UTC timestamps.
93         """
94         self._convert_to_localtime = not use_utc
95
96     def set_now(self, timestamp):
97         """Sets the "current time" for the purposes of snapshot expiration.
98
99         timestamp should be a datetime object, expressed in UTC.  If set_now()
100         is not called, the current time defaults to the time at which the
101         RetentionEngine object was instantiated.
102         """
103         self._now = timestamp
104
105     def add_policy(self, backup_class, retention_period):
106         self._policies[backup_class] = retention_period
107         self._last_snapshots[backup_class] = (None, None, False)
108
109     @staticmethod
110     def parse_timestamp(s):
111         if isinstance(s, datetime.datetime):
112             return s
113         return datetime.datetime.strptime(s, TIMESTAMP_FORMAT)
114
115     def consider_snapshot(self, snapshot):
116         """Compute whether a given snapshot should be expired.
117
118         Successive calls to consider_snapshot() must be for snapshots in
119         chronological order.  For each call, consider_snapshot() will return a
120         boolean indicating whether the snapshot should be retained (True) or
121         expired (False).
122         """
123         timestamp_utc = self.parse_timestamp(snapshot)
124         snapshot_age = self._now - timestamp_utc
125
126         # timestamp_policy is the timestamp in the format that will be used for
127         # doing policy matching: either in the local timezone or UTC, depending
128         # on the setting of set_utc().
129         if self._convert_to_localtime:
130             unixtime = calendar.timegm(timestamp_utc.timetuple())
131             timestamp_policy = datetime.datetime.fromtimestamp(unixtime)
132         else:
133             timestamp_policy = timestamp_utc
134
135         self._labels = set()
136         retain = False
137         for (backup_class, retention_period) in self._policies.items():
138             partition = _backup_classes[backup_class](timestamp_policy)
139             last_snapshot = self._last_snapshots[backup_class]
140             if self._last_snapshots[backup_class][0] != partition:
141                 self._labels.add(backup_class)
142                 retain_label = snapshot_age < retention_period
143                 self._last_snapshots[backup_class] = (partition, snapshot,
144                                                       retain_label)
145                 if retain_label: retain = True
146         return retain
147
148     def last_labels(self):
149         """Return the set of policies that applied to the last snapshot.
150
151         This will fail if consider_snapshot has not yet been called.
152         """
153         return self._labels
154
155     def last_snapshots(self):
156         """Returns the most recent snapshot in each backup class."""
157         return dict((k, v[1]) for (k, v)
158                     in self._last_snapshots.items() if v[2])