blob: c38cf9b3a79fa5dc086af3fe4620544e0a061997 [file] [log] [blame]
Max Rasskazov26787df2015-06-05 14:47:27 +03001#-*- coding: utf-8 -*-
2
3import datetime
Max Rasskazov26787df2015-06-05 14:47:27 +03004
5import utils
6
7from rsync_remote import RsyncRemote
8from utils import singleton
9
10
11@singleton
12class TimeStamp(object):
Max Rasskazov83e10a52015-06-18 14:08:12 +030013 def __init__(self, now=None):
14 # now='2015-06-18-104259'
15 self.snapshot_stamp_format = r'%Y-%m-%d-%H%M%S'
16 self.snapshot_stamp_regexp = r'[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}'
17
18 if now is None:
19 self.now = datetime.datetime.utcnow()
20 else:
21 self.now = datetime.datetime.strptime(now,
22 self.snapshot_stamp_format)
23 self.snapshot_stamp = self.now.strftime(self.snapshot_stamp_format)
Max Rasskazov26787df2015-06-05 14:47:27 +030024
25 def __str__(self):
Max Rasskazov83e10a52015-06-18 14:08:12 +030026 return self.snapshot_stamp
Max Rasskazov26787df2015-06-05 14:47:27 +030027
Max Rasskazovd77a5e62015-06-19 17:47:30 +030028 def reinit(self, *args, **kwagrs):
29 self.__init__(*args, **kwagrs)
30
Max Rasskazov26787df2015-06-05 14:47:27 +030031
Max Rasskazov3e837582015-06-17 18:44:15 +030032class TRsync(RsyncRemote):
Max Rasskazov6d7b5c82015-07-13 13:31:58 +030033 # TODO: possible check that rsync url is exists
Max Rasskazov26787df2015-06-05 14:47:27 +030034 def __init__(self,
35 rsync_url,
Max Rasskazov01271622015-06-17 02:35:09 +030036 snapshot_dir='snapshots',
37 latest_successful_postfix='latest',
Max Rasskazov26787df2015-06-05 14:47:27 +030038 save_latest_days=14,
39 init_directory_structure=True,
Max Rasskazov83e10a52015-06-18 14:08:12 +030040 timestamp=None,
Max Rasskazov26787df2015-06-05 14:47:27 +030041 ):
Max Rasskazov3e837582015-06-17 18:44:15 +030042 super(TRsync, self).__init__(rsync_url)
43 self.logger = utils.logger.getChild('TRsync.' + rsync_url)
Max Rasskazov83e10a52015-06-18 14:08:12 +030044 self.timestamp = TimeStamp(timestamp)
Max Rasskazov26787df2015-06-05 14:47:27 +030045 self.logger.info('Using timestamp {}'.format(self.timestamp))
Max Rasskazov46cc0732015-06-05 19:23:24 +030046 self.snapshot_dir = self.url.a_dir(snapshot_dir)
Max Rasskazov26787df2015-06-05 14:47:27 +030047 self.latest_successful_postfix = latest_successful_postfix
48 self.save_latest_days = save_latest_days
49
Max Rasskazov26787df2015-06-05 14:47:27 +030050 if init_directory_structure is True:
51 self.init_directory_structure()
52
53 def init_directory_structure(self):
Max Rasskazov01271622015-06-17 02:35:09 +030054 if self.url.url_type != 'path':
55 server_root = RsyncRemote(self.url.root)
56 return server_root.mkdir(
57 self.url.a_dir(self.url.path, self.snapshot_dir)
58 )
Max Rasskazov26787df2015-06-05 14:47:27 +030059
Max Rasskazov501cea52015-06-29 18:11:25 +030060 def push(self, source, repo_name, symlinks=[], extra=None, save_diff=True):
Max Rasskazov46cc0732015-06-05 19:23:24 +030061 latest_path = self.url.a_file(
Max Rasskazov26787df2015-06-05 14:47:27 +030062 self.snapshot_dir,
Max Rasskazov46cc0732015-06-05 19:23:24 +030063 '{}-{}'.format(self.url.a_file(repo_name),
Max Rasskazov26787df2015-06-05 14:47:27 +030064 self.latest_successful_postfix)
65 )
Max Rasskazovdc8df002015-06-25 19:37:41 +030066
67 symlinks = list(symlinks)
68 symlinks.insert(0, latest_path)
69
Max Rasskazov46cc0732015-06-05 19:23:24 +030070 snapshot_name = self.url.a_file(
71 '{}-{}'.format(self.url.a_file(repo_name), self.timestamp)
Max Rasskazov26787df2015-06-05 14:47:27 +030072 )
Max Rasskazov46cc0732015-06-05 19:23:24 +030073 repo_path = self.url.a_file(self.snapshot_dir, snapshot_name)
Max Rasskazov26787df2015-06-05 14:47:27 +030074
Max Rasskazov854399e2015-06-05 16:35:17 +030075 extra = '--link-dest={}'.format(
Max Rasskazov46cc0732015-06-05 19:23:24 +030076 self.url.a_file(self.url.path, latest_path)
Max Rasskazov854399e2015-06-05 16:35:17 +030077 )
Max Rasskazovb1b50832015-06-18 11:24:53 +030078
Max Rasskazovb1b50832015-06-18 11:24:53 +030079 # TODO: split transaction run (push or pull), and
80 # commit/rollback functions. transaction must has possibility to
81 # rollback after commit for implementation of working with pool
82 # of servers. should be something like this:
83 # transactions = list()
84 # result = True
85 # for server in servers:
86 # transactions.append(server.push(source, repo_name))
87 # result = result and transactions[-1].success
88 # if result is True:
89 # for transaction in transactions:
90 # transaction.commit()
91 # result = result and transactions[-1].success
92 # if result is False:
93 # for transaction in transactions:
94 # transaction.rollback()
Max Rasskazova5911852015-06-17 18:29:58 +030095 transaction = list()
96 try:
97 # start transaction
Max Rasskazov3e837582015-06-17 18:44:15 +030098 result = super(TRsync, self).push(source, repo_path, extra)
Max Rasskazovdc8df002015-06-25 19:37:41 +030099 transaction.append(lambda p=repo_path: self.rmdir(p))
Max Rasskazova5911852015-06-17 18:29:58 +0300100 self.logger.info('{}'.format(result))
101
Max Rasskazov501cea52015-06-29 18:11:25 +0300102 if save_diff is True:
103 diff_file = self.tmp.get_file(content='{}'.format(result))
104 diff_file_name = '{}.diff.txt'.format(repo_path)
105 super(TRsync, self).push(diff_file, diff_file_name, extra)
106 transaction.append(lambda f=diff_file_name: self.rmfile(f))
107 self.logger.debug('Diff file {} created.'
108 ''.format(diff_file_name))
109
Max Rasskazovdc8df002015-06-25 19:37:41 +0300110 for symlink in symlinks:
111 try:
112 tgt = [_[1] for _ in self.ls_symlinks(symlink)][0]
113 self.logger.info('Previous {} -> {}'.format(symlink, tgt))
114 undo = lambda l=symlink, t=tgt: self.symlink(l, t)
115 except:
116 undo = lambda l=symlink: self.rmfile(l)
117 # TODO: implement detection of target relative symlink
118 if symlink.startswith(self.snapshot_dir):
119 self.symlink(symlink, snapshot_name)
120 else:
121 self.symlink(symlink, repo_path)
122 transaction.append(undo)
Max Rasskazova5911852015-06-17 18:29:58 +0300123
Max Rasskazovad1518a2015-06-18 11:24:16 +0300124 except RuntimeError:
Max Rasskazovdc8df002015-06-25 19:37:41 +0300125 self.logger.error("Rollback transaction because some of sync"
126 "operation failed")
127 [_() for _ in reversed(transaction)]
128 raise
129
130 try:
Max Rasskazova5911852015-06-17 18:29:58 +0300131 # deleting of old snapshots ignored when assessing the transaction
132 # only warning
Max Rasskazovdc8df002015-06-25 19:37:41 +0300133 self._remove_old_snapshots(repo_name)
134 except RuntimeError:
135 self.logger.warn("Old snapshots are not deleted. Ignore. "
136 "May be next time.")
Max Rasskazova5911852015-06-17 18:29:58 +0300137
Max Rasskazov26787df2015-06-05 14:47:27 +0300138 return result
Max Rasskazov452138b2015-06-17 02:37:34 +0300139
140 def _remove_old_snapshots(self, repo_name, save_latest_days=None):
141 if save_latest_days is None:
142 save_latest_days = self.save_latest_days
143 if save_latest_days is None or save_latest_days is False:
144 # delete all snapshots
145 self.logger.info('Deletion all of the old snapshots '
146 '(save_latest_days == {})'
147 ''.format(save_latest_days))
148 save_latest_days = -1
149 elif save_latest_days == 0:
150 # skipping deletion
151 self.logger.info('Skip deletion of old snapshots '
152 '(save_latest_days == {})'
153 ''.format(save_latest_days))
154 return
155 else:
156 # delete snapshots older than
157 self.logger.info('Deletion all of the unlinked snapshots older '
158 'than {0} days (save_latest_days == {0})'
159 ''.format(save_latest_days))
160 warn_date = \
161 self.timestamp.now - datetime.timedelta(days=save_latest_days)
162 warn_date = datetime.datetime.combine(warn_date, datetime.time(0))
163 snapshots = self.ls_dirs(
164 self.url.a_dir(self.snapshot_dir),
165 pattern=r'^{}-{}$'.format(
166 repo_name,
Max Rasskazov83e10a52015-06-18 14:08:12 +0300167 self.timestamp.snapshot_stamp_regexp
Max Rasskazov452138b2015-06-17 02:37:34 +0300168 )
169 )
170 links = self.ls_symlinks(self.url.a_dir())
171 links += self.ls_symlinks(self.url.a_dir(self.snapshot_dir))
172 for s in snapshots:
173 s_date = datetime.datetime.strptime(
174 s,
175 '{}-{}'.format(repo_name,
Max Rasskazov83e10a52015-06-18 14:08:12 +0300176 self.timestamp.snapshot_stamp_format)
Max Rasskazov452138b2015-06-17 02:37:34 +0300177 )
178 s_date = datetime.datetime.combine(s_date, datetime.time(0))
179 s_path = self.url.a_dir(self.snapshot_dir, s)
180 if s_date < warn_date:
181 s_links = [_[0] for _ in links
182 if _[1] == s
183 or _[1].endswith('/{}'.format(s))
184 ]
185 if not s_links:
186 self.rmdir(s_path)
Max Rasskazov052e9112015-06-29 18:43:15 +0300187 self.rmfile(s_path + '.target.txt')
188 self.rmfile(s_path + '.diff.txt')
Max Rasskazov452138b2015-06-17 02:37:34 +0300189 else:
190 self.logger.info('Skip deletion of "{}" because there are '
191 'symlinks found: {}'.format(s, s_links))
192 else:
193 self.logger.info('Skip deletion of "{}" because it newer than '
194 '{} days'.format(s, save_latest_days))