forked from netfarm/archiver
-
Notifications
You must be signed in to change notification settings - Fork 0
/
backend_vfsimage.py
338 lines (276 loc) · 11.7 KB
/
backend_vfsimage.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
#!/usr/bin/env python
# -*- Mode: Python; tab-width: 4 -*-
#
# Netfarm Mail Archiver - release 2
#
# Copyright (C) 2005-2007 Gianluigi Tiesi <sherpya@netfarm.it>
# Copyright (C) 2007 Gianni Giaccherini <jacketta@netfarm.it>
# Copyright (C) 2005-2007 NetFarm S.r.l. [http://www.netfarm.it]
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
# ======================================================================
## @file backend_vfsimage.py
## VFS Image Storage only Backend
__doc__ = '''Netfarm Archiver - release 2.1.0 - VFS Image backend'''
__version__ = '2.1.0'
__all__ = [ 'Backend' ]
from archiver import *
from sys import platform, exc_info
from os import path, access, makedirs, stat, F_OK, R_OK, W_OK
from os import unlink, rename
from errno import ENOSPC
from anydbm import open as opendb
from ConfigParser import ConfigParser
from popen2 import Popen4
from compress import CompressedFile, compressors
from backend_pgsql import sql_quote, format_msg, Backend as BackendPGSQL
### /etc/sudoers
# user ALL = NOPASSWD:/bin/mount,/bin/umount,/usr/bin/install
### Constants
cmd_mke2fs='/sbin/mke2fs -j -q -F -T news -L %(label)s -m 0 -O dir_index %(image)s'
cmd_tune2fs='/sbin/tune2fs -O ^has_journal %(image)s'
cmd_mount='/usr/bin/sudo /bin/mount -t ext3 -o loop %(image)s %(mountpoint)s'
cmd_umount='/usr/bin/sudo /bin/umount %(mountpoint)s'
cmd_prepare='/usr/bin/sudo /usr/bin/install -d -m 755 -o %(user)s %(mountpoint)s/%(archiverdir)s'
##
update_query = 'update mail set media = get_curr_media() where year = %(year)d and pid = %(pid)d;'
class VFSError(Exception):
pass
class Backend(BackendPGSQL):
"""VFS Image Backend Class
Stores emails on filesystem image"""
def __init__(self, config, stage_type, ar_globals):
"""The constructor"""
self._prefix = 'VFSImage Backend: '
### Init PGSQL Backend
BackendPGSQL.__init__(self, config, 'archive', ar_globals, self._prefix)
# Avoid any chance to call uneeded methods
self.process_archive = None
self.parse_recipients = None
self.config = config
self.type = stage_type
if self.type != 'storage':
raise StorageTypeNotSupported, self.type
self.LOG = ar_globals['LOG']
self.user = ar_globals['runas']
if platform.find('linux') == -1:
raise VFSError, 'This backend only works on Linux'
error = None
try:
self.imagebase= config.get(self.type, 'imagebase')
self.mountpoint = config.get(self.type, 'mountpoint')
self.label = config.get(self.type, 'label')
self.archiverdir = config.get(self.type, 'archiverdir')
self.imagesize = config.getint(self.type, 'imagesize')
except:
t, val, tb = exc_info()
del t, tb
error = str(val)
if error is not None:
self.LOG(E_ERR, self._prefix + 'Bad config file: %s' % error)
raise BadConfig
self.image = self.imagebase + '.img'
try:
self.compression = config.get(self.type, 'compression')
except:
self.compression = None
error = None
if self.compression is not None:
try:
compressor, ratio = self.compression.split(':')
ratio = int(ratio)
if ratio < 0 or ratio > 9:
error = 'Invalid compression ratio'
elif not compressors.has_key(compressor.lower()):
error = 'Compression type not supported'
self.compression = (compressor, ratio)
except:
error = 'Unparsable compression entry in config file'
if error is not None:
self.LOG(E_ERR, self._prefix + 'Invalid compression option: %s' % error)
raise BadConfig, 'Invalid compression option'
if not access(self.mountpoint, F_OK | R_OK | W_OK):
self.LOG(E_ERR, self._prefix + 'Mount point is not accessible: %s' % self.mountpoint)
raise VFSError, 'Mount point is not accessible'
if self.isMounted():
self.LOG(E_ERR, self._prefix + 'Image already mounted')
if not self.umount():
raise VFSError, 'Cannot umount image'
isPresent = True
try:
stat(self.image)
except:
isPresent = False
if isPresent and not self.initImage():
raise VFSError, 'Cannot init Image'
else:
self.LOG(E_ALWAYS, self._prefix + 'Image init postponed')
self.LOG(E_ALWAYS, self._prefix + '(%s) at %s' % (self.type, self.image))
def initImage(self):
if not self.mount():
self.LOG(E_ERR, self._prefix + 'Cannot mount image')
return False
if not self.prepare():
self.LOG(E_ERR, self._prefix + 'Image preparation failed')
return False
return True
def isMounted(self):
try:
mounts = open('/proc/mounts').readlines()
except:
self.LOG(E_ERR, self._prefix + 'Cannot open /proc/mounts, /proc not mounted?')
return False
for mp in mounts:
mp = mp.strip().split()
if mp[1] == self.mountpoint:
return True
return False
def getImageFile(self):
res, data, msg = self.do_query('select get_curr_media();', fetch=True, autorecon=True)
self.LOG(E_ERR, data)
if not res or len(data) != 1:
self.LOG(E_ERR, self._prefix + 'GetImageFile failed ' + msg)
return None
return '%s-%d.img' % (self.imagebase, data[0]) # Media Id
def do_cmd(self, cmd, text):
self.LOG(E_TRACE, self._prefix + 'Executing [%s]' % cmd)
pipe = Popen4(cmd)
code = pipe.wait()
res = pipe.fromchild.read()
if code:
self.LOG(E_ERR, self._prefix + '%s (%s)' % (text, res.strip()))
return False
self.LOG(E_TRACE, self._prefix + 'Command output: [%s]' % res.strip())
return True
def mount(self):
return self.do_cmd(cmd_mount % { 'image' : self.image, 'mountpoint' : self.mountpoint }, 'Cannot mount image')
def umount(self):
return self.do_cmd(cmd_umount % { 'mountpoint' : self.mountpoint }, 'Cannot umount image')
def create(self):
res, data, msg = self.do_query('select get_next_media();', fetch=True, autorecon=True)
if not res or (len(data) != 1) or data[0] == 0:
self.LOG(E_ERR, self._prefix + 'Query returned unexpected data [%s]' % msg)
return False
media_id = str(data[0])
label = '-'.join([self.label, str(media_id)])
try:
fd = open(self.image, 'wb')
fd.seek((self.imagesize * 1024 * 1024) - 1)
fd.write(chr(0))
fd.close()
except:
self.LOG(E_ERR, self._prefix + 'Cannot create the image file')
return False
if self.do_cmd(cmd_mke2fs % { 'label' : label, 'image' : self.image }, 'Cannot make image'):
return True
return False
def prepare(self):
return self.do_cmd(cmd_prepare % { 'user': self.user,
'mountpoint': self.mountpoint,
'archiverdir': self.archiverdir },
'Cannot prepare image for archiver')
def reseal(self):
return self.do_cmd(cmd_tune2fs % { 'image': self.image }, 'Cannot remove journal from image')
def recycle(self):
if not self.umount():
self.LOG(E_ERR, self._prefix + 'Recycle: umount failed')
return False
if not self.reseal():
self.LOG(E_ERR, self._prefix + 'Recycle: reseal failed')
return False
newname = self.getImageFile()
if newname is None:
self.LOG(E_ERR, self._prefix + 'Recycle: cannot get image name')
return False
try:
rename(self.image, newname)
except:
self.LOG(E_ERR, self._prefix + 'Recycle: rename failed')
return False
return True
## Gets mailpath and filename
def get_paths(self, data):
month = data['date'][1]
mailpath = path.join(self.mountpoint, self.archiverdir, str(data['year']), str(month))
filename = path.join(mailpath, str(data['pid']))
return mailpath, filename
## Storage on filesystem
def process(self, data):
mailpath, filename = self.get_paths(data)
try:
stat(self.image)
except:
self.LOG(E_ALWAYS, self._prefix + 'Image not present, creating it')
if not self.create():
self.LOG(E_ERR, self._prefix + 'Cannot create Image file')
return 0, 443, 'Internal Error (Image creation failed)'
if not self.initImage():
self.LOG(E_ERR, self._prefix + 'Cannot init Image')
return 0, 443, 'Internal Error (Cannot init Image)'
error = None
if not access(mailpath, F_OK | R_OK | W_OK):
error = 'No access to mailpath'
try:
makedirs(mailpath, 0700)
error = None
except:
t, val, tb = exc_info()
del tb
error = '%s: %s' % (t, val)
self.LOG(E_ERR, self._prefix + 'Cannot create storage directory: %s' % str(val))
if error is not None:
return 0, 443, error
if self.compression is not None:
name = '%d-%d.eml' % (data['year'], data['pid'])
comp = CompressedFile(compressor=self.compression[0], ratio=self.compression[1], name=name)
comp.write(data['mail'])
data['mail'] = comp.getdata()
comp.close()
error_no = 0
try:
fd = open(filename, 'wb')
fd.write(data['mail'])
except:
t, val, tb = exc_info()
error_no = val.errno
try: fd.close()
except: pass
## An error occurred unlink file and check if the volume is full
if error_no:
try: unlink(filename)
except: pass
if error_no == ENOSPC:
if not self.recycle():
self.LOG(E_ERR, self._prefix + 'Error recycling Image')
return 0, 443, 'Internal Error (Recycling failed)'
self.LOG(E_ALWAYS, self._prefix + 'Recycled Image File, write postponed')
return 0, 443, 'Recycling volume'
else:
self.LOG(E_ERR, self._prefix + 'Cannot write mail file: %s' % str(val))
return 0, 443, '%s: %s' % (t, val)
self.LOG(E_TRACE, self._prefix + 'wrote %s' % filename)
res, data, msg = self.do_query(update_query % data, fetch=False, autorecon=True)
if not res:
try: unlink(filename)
except: pass
self.LOG(E_ERR, self._prefix + 'Error updating mail entry [%s], removing file from Image' % msg)
return 0, 443, 'Internal Error while updating mail entry'
return BACKEND_OK
def shutdown(self):
"""Backend Shutdown callback"""
self.LOG(E_ALWAYS, self._prefix + '(%s): shutting down' % self.type)
try:
stat(self.image)
self.umount()
except:
pass
## Shutdown PGSQL Backend
BackendPGSQL.shutdown(self)