blob: 39e12034151589c91bf86d8e8db97c652926c66c [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
28
Max Rasskazov3e837582015-06-17 18:44:15 +030029class TRsync(RsyncRemote):
Max Rasskazov26787df2015-06-05 14:47:27 +030030 # retry and other function with mirror
31 # add all the needed directory functions here, like mkdir, ls, rm etc
32 # possible check that rsync url is exists
33 def __init__(self,
34 rsync_url,
Max Rasskazov01271622015-06-17 02:35:09 +030035 snapshot_dir='snapshots',
36 latest_successful_postfix='latest',
Max Rasskazov26787df2015-06-05 14:47:27 +030037 save_latest_days=14,
38 init_directory_structure=True,
Max Rasskazov83e10a52015-06-18 14:08:12 +030039 timestamp=None,
Max Rasskazov26787df2015-06-05 14:47:27 +030040 ):
Max Rasskazov3e837582015-06-17 18:44:15 +030041 super(TRsync, self).__init__(rsync_url)
42 self.logger = utils.logger.getChild('TRsync.' + rsync_url)
Max Rasskazov83e10a52015-06-18 14:08:12 +030043 self.timestamp = TimeStamp(timestamp)
Max Rasskazov26787df2015-06-05 14:47:27 +030044 self.logger.info('Using timestamp {}'.format(self.timestamp))
Max Rasskazov46cc0732015-06-05 19:23:24 +030045 self.snapshot_dir = self.url.a_dir(snapshot_dir)
Max Rasskazov26787df2015-06-05 14:47:27 +030046 self.latest_successful_postfix = latest_successful_postfix
47 self.save_latest_days = save_latest_days
48
Max Rasskazov26787df2015-06-05 14:47:27 +030049 if init_directory_structure is True:
50 self.init_directory_structure()
51
52 def init_directory_structure(self):
53 # TODO: self.rsyncRemote.mkdir
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
60 def push(self, source, repo_name, extra=None):
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 Rasskazov46cc0732015-06-05 19:23:24 +030066 snapshot_name = self.url.a_file(
67 '{}-{}'.format(self.url.a_file(repo_name), self.timestamp)
Max Rasskazov26787df2015-06-05 14:47:27 +030068 )
Max Rasskazov46cc0732015-06-05 19:23:24 +030069 repo_path = self.url.a_file(self.snapshot_dir, snapshot_name)
Max Rasskazov26787df2015-06-05 14:47:27 +030070
Max Rasskazov854399e2015-06-05 16:35:17 +030071 extra = '--link-dest={}'.format(
Max Rasskazov46cc0732015-06-05 19:23:24 +030072 self.url.a_file(self.url.path, latest_path)
Max Rasskazov854399e2015-06-05 16:35:17 +030073 )
Max Rasskazovb1b50832015-06-18 11:24:53 +030074
Max Rasskazova5911852015-06-17 18:29:58 +030075 # TODO: retry on base class!!!!!!!!!!!!!!!
76 # TODO: locking - symlink dir-timestamp.lock -> dir-timestamp
Max Rasskazovb1b50832015-06-18 11:24:53 +030077 # TODO: write status file with symlink info
78 # TODO: split transaction run (push or pull), and
79 # commit/rollback functions. transaction must has possibility to
80 # rollback after commit for implementation of working with pool
81 # of servers. should be something like this:
82 # transactions = list()
83 # result = True
84 # for server in servers:
85 # transactions.append(server.push(source, repo_name))
86 # result = result and transactions[-1].success
87 # if result is True:
88 # for transaction in transactions:
89 # transaction.commit()
90 # result = result and transactions[-1].success
91 # if result is False:
92 # for transaction in transactions:
93 # transaction.rollback()
Max Rasskazova5911852015-06-17 18:29:58 +030094 transaction = list()
95 try:
96 # start transaction
Max Rasskazov3e837582015-06-17 18:44:15 +030097 result = super(TRsync, self).push(source, repo_path, extra)
Max Rasskazova5911852015-06-17 18:29:58 +030098 transaction.append('repo_dir_created')
99 self.logger.info('{}'.format(result))
100
101 try:
102 old_repo_name_symlink_target = \
103 [_[1] for _ in self.ls_symlinks(repo_name)][0]
104 self.logger.info('Previous {} -> {}'
105 ''.format(repo_name,
106 old_repo_name_symlink_target))
107 status = 'updated'
108 except:
109 status = 'created'
110 self.symlink(repo_name, repo_path)
111 transaction.append('symlink_repo_name_{}'.format(status))
112
113 try:
114 old_latest_path_symlink_target = \
115 [_[1] for _ in self.ls_symlinks(latest_path)][0]
116 self.logger.info('Previous {} -> {}'
117 ''.format(latest_path,
118 old_latest_path_symlink_target))
119 status = 'updated'
120 except:
121 status = 'created'
122 self.symlink(latest_path, snapshot_name)
123 transaction.append('symlink_latest_path_{}'.format(status))
124
125 self._remove_old_snapshots(repo_name)
126 transaction.append('old_snapshots_deleted')
127
Max Rasskazovad1518a2015-06-18 11:24:16 +0300128 except RuntimeError:
Max Rasskazova5911852015-06-17 18:29:58 +0300129 # deleting of old snapshots ignored when assessing the transaction
130 # only warning
131 if 'old_snapshots_deleted' not in transaction:
132 self.logger.warn("Old snapshots are not deleted. Ignore. "
133 "May be next time.")
134 transaction.append('old_snapshots_deleted')
135
136 if len(transaction) < 4:
137 # rollback transaction if some of sync operations failed
138
139 if 'symlink_latest_path_updated' in transaction:
140 self.logger.info('Restoring symlink {} -> {}'
141 ''.format(latest_path,
142 old_latest_path_symlink_target))
143 self.symlink(latest_path, old_latest_path_symlink_target)
144 elif 'symlink_latest_path_created' in transaction:
145 self.logger.info('Deleting symlink {}'.format(latest_path))
146 self.rmfile(latest_path)
147
148 if 'symlink_repo_name_updated' in transaction:
149 self.logger.info('Restoring symlink {} -> {}'
150 ''.format(repo_name,
151 old_repo_name_symlink_target))
152 self.symlink(repo_name, old_repo_name_symlink_target)
153 elif 'symlink_repo_name_created' in transaction:
154 self.logger.info('Deleting symlink {}'.format(repo_name))
155 self.rmfile(repo_name)
156
157 if 'repo_dir_created' in transaction:
158 self.logger.info('Removing snapshot {}'.format(repo_path))
159 self.rmdir(repo_path)
160 raise
161
Max Rasskazov26787df2015-06-05 14:47:27 +0300162 return result
Max Rasskazov452138b2015-06-17 02:37:34 +0300163
164 def _remove_old_snapshots(self, repo_name, save_latest_days=None):
165 if save_latest_days is None:
166 save_latest_days = self.save_latest_days
167 if save_latest_days is None or save_latest_days is False:
168 # delete all snapshots
169 self.logger.info('Deletion all of the old snapshots '
170 '(save_latest_days == {})'
171 ''.format(save_latest_days))
172 save_latest_days = -1
173 elif save_latest_days == 0:
174 # skipping deletion
175 self.logger.info('Skip deletion of old snapshots '
176 '(save_latest_days == {})'
177 ''.format(save_latest_days))
178 return
179 else:
180 # delete snapshots older than
181 self.logger.info('Deletion all of the unlinked snapshots older '
182 'than {0} days (save_latest_days == {0})'
183 ''.format(save_latest_days))
184 warn_date = \
185 self.timestamp.now - datetime.timedelta(days=save_latest_days)
186 warn_date = datetime.datetime.combine(warn_date, datetime.time(0))
187 snapshots = self.ls_dirs(
188 self.url.a_dir(self.snapshot_dir),
189 pattern=r'^{}-{}$'.format(
190 repo_name,
Max Rasskazov83e10a52015-06-18 14:08:12 +0300191 self.timestamp.snapshot_stamp_regexp
Max Rasskazov452138b2015-06-17 02:37:34 +0300192 )
193 )
194 links = self.ls_symlinks(self.url.a_dir())
195 links += self.ls_symlinks(self.url.a_dir(self.snapshot_dir))
196 for s in snapshots:
197 s_date = datetime.datetime.strptime(
198 s,
199 '{}-{}'.format(repo_name,
Max Rasskazov83e10a52015-06-18 14:08:12 +0300200 self.timestamp.snapshot_stamp_format)
Max Rasskazov452138b2015-06-17 02:37:34 +0300201 )
202 s_date = datetime.datetime.combine(s_date, datetime.time(0))
203 s_path = self.url.a_dir(self.snapshot_dir, s)
204 if s_date < warn_date:
205 s_links = [_[0] for _ in links
206 if _[1] == s
207 or _[1].endswith('/{}'.format(s))
208 ]
209 if not s_links:
210 self.rmdir(s_path)
211 else:
212 self.logger.info('Skip deletion of "{}" because there are '
213 'symlinks found: {}'.format(s, s_links))
214 else:
215 self.logger.info('Skip deletion of "{}" because it newer than '
216 '{} days'.format(s, save_latest_days))