~mwilck/duplicity/duplicity

« back to all changes in this revision

Viewing changes to duplicity/backends/multibackend.py

  • Committer: Aaron A Whitehouse
  • Date: 2016-07-02 17:12:58 UTC
  • mfrom: (1111 duplicity-src8)
  • mto: This revision was merged to the branch mainline in revision 1113.
  • Revision ID: lists@whitehouse.kiwi.nz-20160702171258-xhnf5v5g542fcfhq
Merge with trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
2
2
#
3
3
# Copyright 2015 Steve Tynor <steve.tynor@gmail.com>
 
4
# Copyright 2016 Thomas Harning Jr <harningt@gmail.com>
 
5
#                  - mirror/stripe modes
 
6
#                  - write error modes
4
7
#
5
8
# This file is part of duplicity.
6
9
#
24
27
import os.path
25
28
import string
26
29
import urllib
 
30
import urlparse
27
31
import json
28
32
 
29
33
import duplicity.backend
32
36
 
33
37
 
34
38
class MultiBackend(duplicity.backend.Backend):
35
 
    """Store files across multiple remote stores. URL is a path to a local file containing URLs/other config defining the remote store"""
 
39
    """Store files across multiple remote stores. URL is a path to a local file
 
40
    containing URLs/other config defining the remote store"""
36
41
 
37
42
    # the stores we are managing
38
43
    __stores = []
39
44
 
40
 
    # when we write, we "stripe" via a simple round-robin across
 
45
    # Set of known query paramaters
 
46
    __knownQueryParameters = frozenset([
 
47
        'mode',
 
48
        'onfail',
 
49
    ])
 
50
 
 
51
    # the mode of operation to follow
 
52
    # can be one of 'stripe' or 'mirror' currently
 
53
    __mode = 'stripe'
 
54
    __mode_allowedSet = frozenset([
 
55
        'mirror',
 
56
        'stripe',
 
57
    ])
 
58
 
 
59
    # the write error handling logic
 
60
    # can be one of the following:
 
61
    # * continue - default, on failure continues to next source
 
62
    # * abort - stop all further operations
 
63
    __onfail_mode = 'continue'
 
64
    __onfail_mode_allowedSet = frozenset([
 
65
        'abort',
 
66
        'continue',
 
67
    ])
 
68
 
 
69
    # when we write in stripe mode, we "stripe" via a simple round-robin across
41
70
    # remote stores.  It's hard to get too much more sophisticated
42
71
    # since we can't rely on the backend to give us any useful meta
43
72
    # data (e.g. sizes of files, capacity of the store (quotas)) to do
44
73
    # a better job of balancing load across stores.
45
74
    __write_cursor = 0
46
75
 
 
76
    @staticmethod
 
77
    def get_query_params(parsed_url):
 
78
        # Reparse so the query string is available
 
79
        reparsed_url = urlparse.urlparse(parsed_url.geturl())
 
80
        if len(reparsed_url.query) == 0:
 
81
            return dict()
 
82
        try:
 
83
            queryMultiDict = urlparse.parse_qs(reparsed_url.query, strict_parsing=True)
 
84
        except ValueError as e:
 
85
            log.Log(_("MultiBackend: Could not parse query string %s: %s ")
 
86
                    % (reparsed_url.query, e),
 
87
                    log.ERROR)
 
88
            raise BackendException('Could not parse query string')
 
89
        queryDict = dict()
 
90
        # Convert the multi-dict to a single dictionary
 
91
        # while checking to make sure that no unrecognized values are found
 
92
        for name, valueList in queryMultiDict.items():
 
93
            if len(valueList) != 1:
 
94
                log.Log(_("MultiBackend: Invalid query string %s: more than one value for %s")
 
95
                        % (reparsed_url.query, name),
 
96
                        log.ERROR)
 
97
                raise BackendException('Invalid query string')
 
98
            if name not in MultiBackend.__knownQueryParameters:
 
99
                log.Log(_("MultiBackend: Invalid query string %s: unknown parameter %s")
 
100
                        % (reparsed_url.query, name),
 
101
                        log.ERROR)
 
102
                raise BackendException('Invalid query string')
 
103
 
 
104
            queryDict[name] = valueList[0]
 
105
        return queryDict
 
106
 
47
107
    def __init__(self, parsed_url):
48
108
        duplicity.backend.Backend.__init__(self, parsed_url)
49
109
 
77
137
        #  }
78
138
        # ]
79
139
 
 
140
        queryParams = MultiBackend.get_query_params(parsed_url)
 
141
 
 
142
        if 'mode' in queryParams:
 
143
            self.__mode = queryParams['mode']
 
144
 
 
145
        if 'onfail' in queryParams:
 
146
            self.__onfail_mode = queryParams['onfail']
 
147
 
 
148
        if self.__mode not in MultiBackend.__mode_allowedSet:
 
149
            log.Log(_("MultiBackend: illegal value for %s: %s")
 
150
                    % ('mode', self.__mode), log.ERROR)
 
151
            raise BackendException("MultiBackend: invalid mode value")
 
152
 
 
153
        if self.__onfail_mode not in MultiBackend.__onfail_mode_allowedSet:
 
154
            log.Log(_("MultiBackend: illegal value for %s: %s")
 
155
                    % ('onfail', self.__onfail_mode), log.ERROR)
 
156
            raise BackendException("MultiBackend: invalid onfail value")
 
157
 
80
158
        try:
81
159
            with open(parsed_url.path) as f:
82
160
                configs = json.load(f)
83
161
        except IOError as e:
 
162
            log.Log(_("MultiBackend: Url %s")
 
163
                    % (parsed_url.geturl()),
 
164
                    log.ERROR)
 
165
 
84
166
            log.Log(_("MultiBackend: Could not load config file %s: %s ")
85
167
                    % (parsed_url.path, e),
86
168
                    log.ERROR)
88
170
 
89
171
        for config in configs:
90
172
            url = config['url']
 
173
            # Fix advised in bug #1471795
 
174
            url = url.encode('utf-8')
91
175
            log.Log(_("MultiBackend: use store %s")
92
176
                    % (url),
93
177
                    log.INFO)
106
190
            #         log.INFO)
107
191
 
108
192
    def _put(self, source_path, remote_filename):
 
193
        # Store an indication of whether any of these passed
 
194
        passed = False
 
195
        # Mirror mode always starts at zero
 
196
        if self.__mode == 'mirror':
 
197
            self.__write_cursor = 0
 
198
 
109
199
        first = self.__write_cursor
110
200
        while True:
111
201
            store = self.__stores[self.__write_cursor]
117
207
                        % (self.__write_cursor, store.backend.parsed_url.url_string),
118
208
                        log.DEBUG)
119
209
                store.put(source_path, remote_filename)
 
210
                passed = True
120
211
                self.__write_cursor = next
121
 
                break
 
212
                # No matter what, if we loop around, break this loop
 
213
                if next == 0:
 
214
                    break
 
215
                # If in stripe mode, don't continue to the next
 
216
                if self.__mode == 'stripe':
 
217
                    break
122
218
            except Exception as e:
123
219
                log.Log(_("MultiBackend: failed to write to store #%s (%s), try #%s, Exception: %s")
124
220
                        % (self.__write_cursor, store.backend.parsed_url.url_string, next, e),
125
221
                        log.INFO)
126
222
                self.__write_cursor = next
127
223
 
128
 
                if (self.__write_cursor == first):
 
224
                # If we consider write failure as abort, abort
 
225
                if self.__onfail_mode == 'abort':
 
226
                    log.Log(_("MultiBackend: failed to write %s. Aborting process.")
 
227
                            % (source_path),
 
228
                            log.ERROR)
 
229
                    raise BackendException("failed to write")
 
230
 
 
231
                # If we've looped around, and none of them passed, fail
 
232
                if (self.__write_cursor == first) and not passed:
129
233
                    log.Log(_("MultiBackend: failed to write %s. Tried all backing stores and none succeeded")
130
234
                            % (source_path),
131
235
                            log.ERROR)
159
263
                    % (s.backend.parsed_url.url_string, l),
160
264
                    log.DEBUG)
161
265
            lists.append(s.list())
162
 
        # combine the lists into a single flat list:
163
 
        result = [item for sublist in lists for item in sublist]
 
266
        # combine the lists into a single flat list w/o duplicates via set:
 
267
        result = list({item for sublist in lists for item in sublist})
164
268
        log.Log(_("MultiBackend: combined list: %s")
165
269
                % (result),
166
270
                log.DEBUG)
167
271
        return result
168
272
 
169
273
    def _delete(self, filename):
 
274
        # Store an indication on whether any passed
 
275
        passed = False
170
276
        # since the backend operations will be retried, we can't
171
277
        # simply try to get from the store, if not found, move to the
172
278
        # next store (since each failure will be retried n times
177
283
            list = s.list()
178
284
            if filename in list:
179
285
                s._do_delete(filename)
180
 
                return
 
286
                passed = True
 
287
                # In stripe mode, only one item will have the file
 
288
                if self.__mode == 'stripe':
 
289
                    return
181
290
            log.Log(_("MultiBackend: failed to delete %s from %s")
182
291
                    % (filename, s.backend.parsed_url.url_string),
183
292
                    log.INFO)
184
 
        log.Log(_("MultiBackend: failed to delete %s. Tried all backing stores and none succeeded")
185
 
                % (filename),
186
 
                log.ERROR)
187
 
#        raise BackendException("failed to delete")
 
293
        if not passed:
 
294
            log.Log(_("MultiBackend: failed to delete %s. Tried all backing stores and none succeeded")
 
295
                    % (filename),
 
296
                    log.ERROR)
 
297
#           raise BackendException("failed to delete")
188
298
 
189
299
duplicity.backend.register_backend('multi', MultiBackend)