blob: 138ddddd8c91f4e9f12b1fdfa401a0d3342b1b3d [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):
13 def __init__(self):
14 self.now = datetime.datetime.utcnow()
15 self.staging_snapshot_stamp_format = r'%Y-%m-%d-%H%M%S'
16 self.staging_snapshot_stamp_regexp = \
17 r'[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{6}'
18 self.staging_snapshot_stamp = \
19 self.now.strftime(self.staging_snapshot_stamp_format)
20
21 def __str__(self):
22 return self.staging_snapshot_stamp
23
24
Max Rasskazov3e837582015-06-17 18:44:15 +030025class TRsync(RsyncRemote):
Max Rasskazov26787df2015-06-05 14:47:27 +030026 # retry and other function with mirror
27 # add all the needed directory functions here, like mkdir, ls, rm etc
28 # possible check that rsync url is exists
Max Rasskazovb1b50832015-06-18 11:24:53 +030029 # TODO: add possibility of specify timestamp as parameter for
30 # manual fixes
Max Rasskazov26787df2015-06-05 14:47:27 +030031 def __init__(self,
32 rsync_url,
Max Rasskazov01271622015-06-17 02:35:09 +030033 snapshot_dir='snapshots',
34 latest_successful_postfix='latest',
Max Rasskazov26787df2015-06-05 14:47:27 +030035 save_latest_days=14,
36 init_directory_structure=True,
37 ):
Max Rasskazov3e837582015-06-17 18:44:15 +030038 super(TRsync, self).__init__(rsync_url)
39 self.logger = utils.logger.getChild('TRsync.' + rsync_url)
Max Rasskazov26787df2015-06-05 14:47:27 +030040 self.timestamp = TimeStamp()
41 self.logger.info('Using timestamp {}'.format(self.timestamp))
Max Rasskazov46cc0732015-06-05 19:23:24 +030042 self.snapshot_dir = self.url.a_dir(snapshot_dir)
Max Rasskazov26787df2015-06-05 14:47:27 +030043 self.latest_successful_postfix = latest_successful_postfix
44 self.save_latest_days = save_latest_days
45
Max Rasskazov26787df2015-06-05 14:47:27 +030046 if init_directory_structure is True:
47 self.init_directory_structure()
48
49 def init_directory_structure(self):
50 # TODO: self.rsyncRemote.mkdir
Max Rasskazov01271622015-06-17 02:35:09 +030051 if self.url.url_type != 'path':
52 server_root = RsyncRemote(self.url.root)
53 return server_root.mkdir(
54 self.url.a_dir(self.url.path, self.snapshot_dir)
55 )
Max Rasskazov26787df2015-06-05 14:47:27 +030056
57 def push(self, source, repo_name, extra=None):
Max Rasskazov46cc0732015-06-05 19:23:24 +030058 latest_path = self.url.a_file(
Max Rasskazov26787df2015-06-05 14:47:27 +030059 self.snapshot_dir,
Max Rasskazov46cc0732015-06-05 19:23:24 +030060 '{}-{}'.format(self.url.a_file(repo_name),
Max Rasskazov26787df2015-06-05 14:47:27 +030061 self.latest_successful_postfix)
62 )
Max Rasskazov46cc0732015-06-05 19:23:24 +030063 snapshot_name = self.url.a_file(
64 '{}-{}'.format(self.url.a_file(repo_name), self.timestamp)
Max Rasskazov26787df2015-06-05 14:47:27 +030065 )
Max Rasskazov46cc0732015-06-05 19:23:24 +030066 repo_path = self.url.a_file(self.snapshot_dir, snapshot_name)
Max Rasskazov26787df2015-06-05 14:47:27 +030067
Max Rasskazov854399e2015-06-05 16:35:17 +030068 extra = '--link-dest={}'.format(
Max Rasskazov46cc0732015-06-05 19:23:24 +030069 self.url.a_file(self.url.path, latest_path)
Max Rasskazov854399e2015-06-05 16:35:17 +030070 )
Max Rasskazovb1b50832015-06-18 11:24:53 +030071
Max Rasskazova5911852015-06-17 18:29:58 +030072 # TODO: retry on base class!!!!!!!!!!!!!!!
73 # TODO: locking - symlink dir-timestamp.lock -> dir-timestamp
Max Rasskazovb1b50832015-06-18 11:24:53 +030074 # TODO: write status file with symlink info
75 # TODO: split transaction run (push or pull), and
76 # commit/rollback functions. transaction must has possibility to
77 # rollback after commit for implementation of working with pool
78 # of servers. should be something like this:
79 # transactions = list()
80 # result = True
81 # for server in servers:
82 # transactions.append(server.push(source, repo_name))
83 # result = result and transactions[-1].success
84 # if result is True:
85 # for transaction in transactions:
86 # transaction.commit()
87 # result = result and transactions[-1].success
88 # if result is False:
89 # for transaction in transactions:
90 # transaction.rollback()
Max Rasskazova5911852015-06-17 18:29:58 +030091 transaction = list()
92 try:
93 # start transaction
Max Rasskazov3e837582015-06-17 18:44:15 +030094 result = super(TRsync, self).push(source, repo_path, extra)
Max Rasskazova5911852015-06-17 18:29:58 +030095 transaction.append('repo_dir_created')
96 self.logger.info('{}'.format(result))
97
98 try:
99 old_repo_name_symlink_target = \
100 [_[1] for _ in self.ls_symlinks(repo_name)][0]
101 self.logger.info('Previous {} -> {}'
102 ''.format(repo_name,
103 old_repo_name_symlink_target))
104 status = 'updated'
105 except:
106 status = 'created'
107 self.symlink(repo_name, repo_path)
108 transaction.append('symlink_repo_name_{}'.format(status))
109
110 try:
111 old_latest_path_symlink_target = \
112 [_[1] for _ in self.ls_symlinks(latest_path)][0]
113 self.logger.info('Previous {} -> {}'
114 ''.format(latest_path,
115 old_latest_path_symlink_target))
116 status = 'updated'
117 except:
118 status = 'created'
119 self.symlink(latest_path, snapshot_name)
120 transaction.append('symlink_latest_path_{}'.format(status))
121
122 self._remove_old_snapshots(repo_name)
123 transaction.append('old_snapshots_deleted')
124
Max Rasskazovad1518a2015-06-18 11:24:16 +0300125 except RuntimeError:
Max Rasskazova5911852015-06-17 18:29:58 +0300126 # deleting of old snapshots ignored when assessing the transaction
127 # only warning
128 if 'old_snapshots_deleted' not in transaction:
129 self.logger.warn("Old snapshots are not deleted. Ignore. "
130 "May be next time.")
131 transaction.append('old_snapshots_deleted')
132
133 if len(transaction) < 4:
134 # rollback transaction if some of sync operations failed
135
136 if 'symlink_latest_path_updated' in transaction:
137 self.logger.info('Restoring symlink {} -> {}'
138 ''.format(latest_path,
139 old_latest_path_symlink_target))
140 self.symlink(latest_path, old_latest_path_symlink_target)
141 elif 'symlink_latest_path_created' in transaction:
142 self.logger.info('Deleting symlink {}'.format(latest_path))
143 self.rmfile(latest_path)
144
145 if 'symlink_repo_name_updated' in transaction:
146 self.logger.info('Restoring symlink {} -> {}'
147 ''.format(repo_name,
148 old_repo_name_symlink_target))
149 self.symlink(repo_name, old_repo_name_symlink_target)
150 elif 'symlink_repo_name_created' in transaction:
151 self.logger.info('Deleting symlink {}'.format(repo_name))
152 self.rmfile(repo_name)
153
154 if 'repo_dir_created' in transaction:
155 self.logger.info('Removing snapshot {}'.format(repo_path))
156 self.rmdir(repo_path)
157 raise
158
Max Rasskazov26787df2015-06-05 14:47:27 +0300159 return result
Max Rasskazov452138b2015-06-17 02:37:34 +0300160
161 def _remove_old_snapshots(self, repo_name, save_latest_days=None):
162 if save_latest_days is None:
163 save_latest_days = self.save_latest_days
164 if save_latest_days is None or save_latest_days is False:
165 # delete all snapshots
166 self.logger.info('Deletion all of the old snapshots '
167 '(save_latest_days == {})'
168 ''.format(save_latest_days))
169 save_latest_days = -1
170 elif save_latest_days == 0:
171 # skipping deletion
172 self.logger.info('Skip deletion of old snapshots '
173 '(save_latest_days == {})'
174 ''.format(save_latest_days))
175 return
176 else:
177 # delete snapshots older than
178 self.logger.info('Deletion all of the unlinked snapshots older '
179 'than {0} days (save_latest_days == {0})'
180 ''.format(save_latest_days))
181 warn_date = \
182 self.timestamp.now - datetime.timedelta(days=save_latest_days)
183 warn_date = datetime.datetime.combine(warn_date, datetime.time(0))
184 snapshots = self.ls_dirs(
185 self.url.a_dir(self.snapshot_dir),
186 pattern=r'^{}-{}$'.format(
187 repo_name,
188 self.timestamp.staging_snapshot_stamp_regexp
189 )
190 )
191 links = self.ls_symlinks(self.url.a_dir())
192 links += self.ls_symlinks(self.url.a_dir(self.snapshot_dir))
193 for s in snapshots:
194 s_date = datetime.datetime.strptime(
195 s,
196 '{}-{}'.format(repo_name,
197 self.timestamp.staging_snapshot_stamp_format)
198 )
199 s_date = datetime.datetime.combine(s_date, datetime.time(0))
200 s_path = self.url.a_dir(self.snapshot_dir, s)
201 if s_date < warn_date:
202 s_links = [_[0] for _ in links
203 if _[1] == s
204 or _[1].endswith('/{}'.format(s))
205 ]
206 if not s_links:
207 self.rmdir(s_path)
208 else:
209 self.logger.info('Skip deletion of "{}" because there are '
210 'symlinks found: {}'.format(s, s_links))
211 else:
212 self.logger.info('Skip deletion of "{}" because it newer than '
213 '{} days'.format(s, save_latest_days))