blob: 4fbf3473c8855391e503f1c4593bb2b3756f8d02 [file] [log] [blame]
Max Rasskazov26787df2015-06-05 14:47:27 +03001#-*- coding: utf-8 -*-
2
3import datetime
Max Rasskazov69c29262015-07-16 19:22:16 +03004import os
Max Rasskazov26787df2015-06-05 14:47:27 +03005
6import utils
7
8from rsync_remote import RsyncRemote
Max Rasskazov3eb11982015-07-16 19:45:17 +03009from utils import TimeStamp
Max Rasskazovd77a5e62015-06-19 17:47:30 +030010
Max Rasskazov26787df2015-06-05 14:47:27 +030011
Max Rasskazov3e837582015-06-17 18:44:15 +030012class TRsync(RsyncRemote):
Max Rasskazov6d7b5c82015-07-13 13:31:58 +030013 # TODO: possible check that rsync url is exists
Max Rasskazov26787df2015-06-05 14:47:27 +030014 def __init__(self,
15 rsync_url,
Max Rasskazov01271622015-06-17 02:35:09 +030016 snapshot_dir='snapshots',
17 latest_successful_postfix='latest',
Max Rasskazov26787df2015-06-05 14:47:27 +030018 save_latest_days=14,
19 init_directory_structure=True,
Max Rasskazov83e10a52015-06-18 14:08:12 +030020 timestamp=None,
Max Rasskazov26787df2015-06-05 14:47:27 +030021 ):
Max Rasskazov3e837582015-06-17 18:44:15 +030022 super(TRsync, self).__init__(rsync_url)
23 self.logger = utils.logger.getChild('TRsync.' + rsync_url)
Max Rasskazov83e10a52015-06-18 14:08:12 +030024 self.timestamp = TimeStamp(timestamp)
Max Rasskazov26787df2015-06-05 14:47:27 +030025 self.logger.info('Using timestamp {}'.format(self.timestamp))
Max Rasskazov46cc0732015-06-05 19:23:24 +030026 self.snapshot_dir = self.url.a_dir(snapshot_dir)
Max Rasskazov26787df2015-06-05 14:47:27 +030027 self.latest_successful_postfix = latest_successful_postfix
28 self.save_latest_days = save_latest_days
29
Max Rasskazov26787df2015-06-05 14:47:27 +030030 if init_directory_structure is True:
31 self.init_directory_structure()
32
33 def init_directory_structure(self):
Max Rasskazov01271622015-06-17 02:35:09 +030034 if self.url.url_type != 'path':
35 server_root = RsyncRemote(self.url.root)
36 return server_root.mkdir(
37 self.url.a_dir(self.url.path, self.snapshot_dir)
38 )
Max Rasskazov26787df2015-06-05 14:47:27 +030039
Max Rasskazov501cea52015-06-29 18:11:25 +030040 def push(self, source, repo_name, symlinks=[], extra=None, save_diff=True):
Max Rasskazov69c29262015-07-16 19:22:16 +030041 repo_basename = os.path.split(repo_name)[-1]
Max Rasskazov46cc0732015-06-05 19:23:24 +030042 latest_path = self.url.a_file(
Max Rasskazov26787df2015-06-05 14:47:27 +030043 self.snapshot_dir,
Max Rasskazov69c29262015-07-16 19:22:16 +030044 '{}-{}'.format(self.url.a_file(repo_basename),
Max Rasskazov26787df2015-06-05 14:47:27 +030045 self.latest_successful_postfix)
46 )
Max Rasskazovdc8df002015-06-25 19:37:41 +030047
48 symlinks = list(symlinks)
49 symlinks.insert(0, latest_path)
50
Max Rasskazov46cc0732015-06-05 19:23:24 +030051 snapshot_name = self.url.a_file(
Max Rasskazov69c29262015-07-16 19:22:16 +030052 '{}-{}'.format(self.url.a_file(repo_basename), self.timestamp)
Max Rasskazov26787df2015-06-05 14:47:27 +030053 )
Max Rasskazov46cc0732015-06-05 19:23:24 +030054 repo_path = self.url.a_file(self.snapshot_dir, snapshot_name)
Max Rasskazov26787df2015-06-05 14:47:27 +030055
Max Rasskazov854399e2015-06-05 16:35:17 +030056 extra = '--link-dest={}'.format(
Max Rasskazov46cc0732015-06-05 19:23:24 +030057 self.url.a_file(self.url.path, latest_path)
Max Rasskazov854399e2015-06-05 16:35:17 +030058 )
Max Rasskazovb1b50832015-06-18 11:24:53 +030059
Max Rasskazovb1b50832015-06-18 11:24:53 +030060 # TODO: split transaction run (push or pull), and
61 # commit/rollback functions. transaction must has possibility to
62 # rollback after commit for implementation of working with pool
63 # of servers. should be something like this:
64 # transactions = list()
65 # result = True
66 # for server in servers:
67 # transactions.append(server.push(source, repo_name))
68 # result = result and transactions[-1].success
69 # if result is True:
70 # for transaction in transactions:
71 # transaction.commit()
72 # result = result and transactions[-1].success
73 # if result is False:
74 # for transaction in transactions:
75 # transaction.rollback()
Max Rasskazova5911852015-06-17 18:29:58 +030076 transaction = list()
77 try:
78 # start transaction
Max Rasskazov3e837582015-06-17 18:44:15 +030079 result = super(TRsync, self).push(source, repo_path, extra)
Max Rasskazovdc8df002015-06-25 19:37:41 +030080 transaction.append(lambda p=repo_path: self.rmdir(p))
Max Rasskazova5911852015-06-17 18:29:58 +030081 self.logger.info('{}'.format(result))
82
Max Rasskazov501cea52015-06-29 18:11:25 +030083 if save_diff is True:
84 diff_file = self.tmp.get_file(content='{}'.format(result))
85 diff_file_name = '{}.diff.txt'.format(repo_path)
86 super(TRsync, self).push(diff_file, diff_file_name, extra)
87 transaction.append(lambda f=diff_file_name: self.rmfile(f))
88 self.logger.debug('Diff file {} created.'
89 ''.format(diff_file_name))
90
Max Rasskazovdc8df002015-06-25 19:37:41 +030091 for symlink in symlinks:
92 try:
93 tgt = [_[1] for _ in self.ls_symlinks(symlink)][0]
94 self.logger.info('Previous {} -> {}'.format(symlink, tgt))
95 undo = lambda l=symlink, t=tgt: self.symlink(l, t)
96 except:
97 undo = lambda l=symlink: self.rmfile(l)
98 # TODO: implement detection of target relative symlink
99 if symlink.startswith(self.snapshot_dir):
100 self.symlink(symlink, snapshot_name)
101 else:
102 self.symlink(symlink, repo_path)
103 transaction.append(undo)
Max Rasskazova5911852015-06-17 18:29:58 +0300104
Max Rasskazovad1518a2015-06-18 11:24:16 +0300105 except RuntimeError:
Max Rasskazovdc8df002015-06-25 19:37:41 +0300106 self.logger.error("Rollback transaction because some of sync"
107 "operation failed")
108 [_() for _ in reversed(transaction)]
109 raise
110
111 try:
Max Rasskazova5911852015-06-17 18:29:58 +0300112 # deleting of old snapshots ignored when assessing the transaction
113 # only warning
Max Rasskazovdc8df002015-06-25 19:37:41 +0300114 self._remove_old_snapshots(repo_name)
115 except RuntimeError:
116 self.logger.warn("Old snapshots are not deleted. Ignore. "
117 "May be next time.")
Max Rasskazova5911852015-06-17 18:29:58 +0300118
Max Rasskazov26787df2015-06-05 14:47:27 +0300119 return result
Max Rasskazov452138b2015-06-17 02:37:34 +0300120
121 def _remove_old_snapshots(self, repo_name, save_latest_days=None):
122 if save_latest_days is None:
123 save_latest_days = self.save_latest_days
124 if save_latest_days is None or save_latest_days is False:
125 # delete all snapshots
126 self.logger.info('Deletion all of the old snapshots '
127 '(save_latest_days == {})'
128 ''.format(save_latest_days))
129 save_latest_days = -1
130 elif save_latest_days == 0:
131 # skipping deletion
132 self.logger.info('Skip deletion of old snapshots '
133 '(save_latest_days == {})'
134 ''.format(save_latest_days))
135 return
136 else:
137 # delete snapshots older than
138 self.logger.info('Deletion all of the unlinked snapshots older '
139 'than {0} days (save_latest_days == {0})'
140 ''.format(save_latest_days))
141 warn_date = \
142 self.timestamp.now - datetime.timedelta(days=save_latest_days)
143 warn_date = datetime.datetime.combine(warn_date, datetime.time(0))
144 snapshots = self.ls_dirs(
145 self.url.a_dir(self.snapshot_dir),
146 pattern=r'^{}-{}$'.format(
147 repo_name,
Max Rasskazov83e10a52015-06-18 14:08:12 +0300148 self.timestamp.snapshot_stamp_regexp
Max Rasskazov452138b2015-06-17 02:37:34 +0300149 )
150 )
151 links = self.ls_symlinks(self.url.a_dir())
152 links += self.ls_symlinks(self.url.a_dir(self.snapshot_dir))
153 for s in snapshots:
154 s_date = datetime.datetime.strptime(
155 s,
156 '{}-{}'.format(repo_name,
Max Rasskazov83e10a52015-06-18 14:08:12 +0300157 self.timestamp.snapshot_stamp_format)
Max Rasskazov452138b2015-06-17 02:37:34 +0300158 )
159 s_date = datetime.datetime.combine(s_date, datetime.time(0))
160 s_path = self.url.a_dir(self.snapshot_dir, s)
161 if s_date < warn_date:
162 s_links = [_[0] for _ in links
163 if _[1] == s
164 or _[1].endswith('/{}'.format(s))
165 ]
166 if not s_links:
167 self.rmdir(s_path)
Max Rasskazov052e9112015-06-29 18:43:15 +0300168 self.rmfile(s_path + '.target.txt')
169 self.rmfile(s_path + '.diff.txt')
Max Rasskazov452138b2015-06-17 02:37:34 +0300170 else:
171 self.logger.info('Skip deletion of "{}" because there are '
172 'symlinks found: {}'.format(s, s_links))
173 else:
174 self.logger.info('Skip deletion of "{}" because it newer than '
175 '{} days'.format(s, save_latest_days))