Add read-only mode that fallbacks on the database

This commit is contained in:
Guewen Baconnier
2016-10-24 17:12:27 +02:00
parent 7825661a1a
commit ec59d275e2
2 changed files with 105 additions and 22 deletions
+16 -1
View File
@@ -12,7 +12,7 @@ With system parameters:
* Create or set the system parameter with the key ``ir_attachment.location`` * Create or set the system parameter with the key ``ir_attachment.location``
and the value in the form ``s3://<access-key>:<secret-key>@<bucket-name>`` and the value in the form ``s3://<access-key>:<secret-key>@<bucket-name>``
* If the host is not AWS services, you can set the key * If the host is not AWS services, you can set the key
``ir_attachment.location.s3host`` to the hostname of the Object Storage ``ir_attachment.s3.host`` to the hostname of the Object Storage
service service
With environment variables: With environment variables:
@@ -24,6 +24,21 @@ With environment variables:
* ``AWS_SECRET_ACCESS_KEY`` * ``AWS_SECRET_ACCESS_KEY``
* ``AWS_BUCKETNAME`` * ``AWS_BUCKETNAME``
Read-only mode:
You can configure the storage to be only for reads on the Object Storage.
This is convenient for replications/tests instances, that will be able to
access to the same content than the production database without any risk to
alter it. The files created or modified the read-only mode is active are
created in the database.
To activate the read-only mode, 2 possibilities:
* create the system parameter ``ir_attachment.s3.readonly`` and set a positive
value (1, true)
* set the environment variable ``AWS_ATTACHMENT_READONLY`` to a positive
value (1, true)
Limitations Limitations
----------- -----------
+89 -21
View File
@@ -9,10 +9,12 @@ import os
import xml.dom.minidom import xml.dom.minidom
from functools import partial from functools import partial
from distutils.util import strtobool
import boto import boto
from boto.exception import S3ResponseError from boto.exception import S3ResponseError
from openerp import _, api, exceptions, models from openerp import _, api, exceptions, fields, models
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -20,6 +22,61 @@ _logger = logging.getLogger(__name__)
class IrAttachment(models.Model): class IrAttachment(models.Model):
_inherit = "ir.attachment" _inherit = "ir.attachment"
datas = fields.Binary(
compute='_compute_datas',
inverse='_inverse_datas',
string='File Content',
nodrop=True,
)
@api.model
def _s3_readonly(self):
def is_true(strval):
return bool(strtobool(strval or '0'.lower()))
params = self.env['ir.config_parameter'].sudo()
storage = params.get_param('ir_attachment.location', default='')
env_ro = is_true(os.environ.get('AWS_ATTACHMENT_READONLY'))
param_ro = is_true(params.get_param('ir_attachment.s3.readonly'))
return storage.startswith('s3://') and (env_ro or param_ro)
@api.depends('store_fname', 'db_datas')
def _compute_datas(self):
bin_size = self._context.get('bin_size')
if self._s3_readonly():
for attach in self:
# look first in db_datas in case a file has been modified
# locally
data = attach.db_datas
if data:
attach.datas = data
else:
params = self.env['ir.config_parameter'].sudo()
bucket_url = params.get_param('ir_attachment.location')
bucket = self._get_s3_bucket(bucket_url)
attach.datas = self._file_read_s3(bucket,
attach.store_fname,
bin_size)
else:
values = self._data_get('datas', None)
for attach in self:
attach.datas = values.get(attach.id)
def _inverse_datas(self):
for attach in self:
self._data_set('datas', attach.datas, None)
@api.model
def _storage(self):
if self._s3_readonly():
# When the S3 readonly mode is active, we force the storage
# to be in the database. We'll override the read method
# to look in S3 if we have a value though.
return 'db'
else:
return super(IrAttachment, self)._storage()
@api.model @api.model
def _get_s3_bucket(self, bucket_url): def _get_s3_bucket(self, bucket_url):
"""Connect to S3 and return the bucket """Connect to S3 and return the bucket
@@ -29,13 +86,13 @@ class IrAttachment(models.Model):
``s3://<access-key>:<secret-key>@<bucket-name>`` ``s3://<access-key>:<secret-key>@<bucket-name>``
Alternatively, we can also use environment variables, in that case, Alternatively, we can also use environment variables, in that case,
you must set the url to ``s3://``. you must set the parameter to ``s3://``.
If the S3 provider is not AWS, the key If the S3 provider is not AWS, the key
``ir_attachment.location.s3host`` can be configured in the System ``ir_attachment.s3.host`` can be configured in the System
Parameters with the hostname. Parameters with the hostname.
The following environment variable can be set: The following environment variables can be set:
* ``AWS_HOST`` * ``AWS_HOST``
* ``AWS_ACCESS_KEY_ID`` * ``AWS_ACCESS_KEY_ID``
* ``AWS_SECRET_ACCESS_KEY`` * ``AWS_SECRET_ACCESS_KEY``
@@ -45,8 +102,8 @@ class IrAttachment(models.Model):
assert bucket_url.startswith('s3://') assert bucket_url.startswith('s3://')
host = os.environ.get('AWS_HOST') host = os.environ.get('AWS_HOST')
if not host: if not host:
host = self.env['ir.config_parameter'].get_param( host = self.env['ir.config_parameter'].sudo().get_param(
'ir_attachment.location.s3host', default=None 'ir_attachment.s3.host', default=None
) )
if host: if host:
connect_s3 = partial(boto.connect_s3, host=host) connect_s3 = partial(boto.connect_s3, host=host)
@@ -63,6 +120,7 @@ class IrAttachment(models.Model):
'* AWS_ACCESS_KEY_ID\n' '* AWS_ACCESS_KEY_ID\n'
'* AWS_SECRET_ACCESS_KEY\n' '* AWS_SECRET_ACCESS_KEY\n'
'* AWS_BUCKETNAME\n' '* AWS_BUCKETNAME\n'
'* AWS_HOST (optional)\n'
) )
) )
else: else:
@@ -103,27 +161,37 @@ class IrAttachment(models.Model):
msg = '%s: %s' % (msg, msg_node[0].childNodes[0].data) msg = '%s: %s' % (msg, msg_node[0].childNodes[0].data)
return msg return msg
@api.model
def _file_read_s3(self, bucket, fname, bin_size=False):
filekey = bucket.get_key(fname)
if filekey:
if bin_size:
read = filekey.size
else:
read = base64.b64encode(filekey.get_contents_as_string())
else:
# If the attachment has been created before the installation
# of the addon, it might be stored on the filesystem.
# Fallback on the filesystem read.
# Consider running ``force_storage()`` to move all the
# attachments on the Object Storage
try:
_super = super(IrAttachment, self)
read = _super._file_read(fname, bin_size=bin_size)
except (IOError, OSError):
# File is missing
read = ''
return read
@api.model @api.model
def _file_read(self, fname, bin_size=False): def _file_read(self, fname, bin_size=False):
_super = super(IrAttachment, self)
storage = self._storage() storage = self._storage()
if storage.startswith('s3://'): if storage.startswith('s3://'):
storage = self._storage()
bucket = self._get_s3_bucket(storage) bucket = self._get_s3_bucket(storage)
filekey = bucket.get_key(fname) read = self._file_read_s3(bucket, fname, bin_size=bin_size)
if filekey:
read = base64.b64encode(filekey.get_contents_as_string())
else:
# If the attachment has been created before the installation
# of the addon, it might be stored on the filesystem.
# Fallback on the filesystem read.
# Consider running ``force_storage()`` to move all the
# attachments on the Object Storage
try:
read = _super._file_read(fname, bin_size=bin_size)
except (IOError, OSError):
# File is missing
return ''
else: else:
_super = super(IrAttachment, self)
read = _super._file_read(fname, bin_size=bin_size) read = _super._file_read(fname, bin_size=bin_size)
return read return read