mirror of
https://github.com/camptocamp/odoo-cloud-platform.git
synced 2026-06-24 16:48:36 +00:00
Merge pull request #371 from vrenaville/azure70
[7.0] Backport module for azure storage
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
===========================================
|
||||
Attachments on Microsoft Azure Blob Storage
|
||||
===========================================
|
||||
|
||||
This addon allows to store the attachments (documents and assets) on `Microsoft Azure
|
||||
Blob Storage <https://docs.microsoft.com/azure/storage/blobs/>`_.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
Activate Azure Blob storage:
|
||||
|
||||
* Create or set the system parameter with the key ``ir_attachment.location``
|
||||
and the value in the form ``azure``.
|
||||
|
||||
Configure accesses with environment variables:
|
||||
|
||||
* ``AZURE_STORAGE_CONNECTION_STRING`` or
|
||||
* ``AZURE_STORAGE_ACCOUNT_NAME``
|
||||
* ``AZURE_STORAGE_ACCOUNT_URL``
|
||||
* ``AZURE_STORAGE_ACCOUNT_KEY``
|
||||
|
||||
One container will be created per database using the `RUNNING_ENV` environment variable
|
||||
and the name of the database. By default, `RUNNING_ENV` is set to `dev`.
|
||||
|
||||
The container name can be overridden with environment variable ``AZURE_STORAGE_NAME``.
|
||||
The strings ``{db}`` and ``{env}`` can be used inside that variable and the values
|
||||
will be replaced respectively by the database name and environment name.
|
||||
|
||||
The container name will also be stored in the database for each attachment,
|
||||
and will be used to access the right container in the storage.
|
||||
|
||||
This addon must be added in the server wide addons with (``--load`` option):
|
||||
|
||||
``--load=web,attachment_azure``
|
||||
|
||||
The System Parameter ``ir_attachment.storage.force.database`` can be customized to
|
||||
force storage of files in the database. See the documentation of the module
|
||||
``base_attachment_object_storage``.
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
* You need to call ``env['ir.attachment'].force_storage()`` after
|
||||
having changed the ``ir_attachment.location`` configuration in order to
|
||||
migrate the existing attachments to Azure Blob Storage.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Copyright 2016-2019 Camptocamp SA
|
||||
# Copyright 2021 Open Source Integrators
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
from . import models
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright 2016-2019 Camptocamp SA
|
||||
# Copyright 2021 Open Source Integrators
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
{
|
||||
"name": "Attachments on Azure storage",
|
||||
"summary": "Store assets and attachments on a Azure compatible object storage",
|
||||
"version": "15.0.1.0.0",
|
||||
"author": "Camptocamp, "
|
||||
"Open Source Integrators, "
|
||||
"Serpent Consulting Services, "
|
||||
"Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"category": "Knowledge Management",
|
||||
"depends": ["base_attachment_object_storage"],
|
||||
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||
"installable": True,
|
||||
"development_status": "Beta",
|
||||
"maintainers": ["max3903"],
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Copyright 2016-2019 Camptocamp SA
|
||||
# Copyright 2021 Open Source Integrators
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
from . import ir_attachment
|
||||
@@ -0,0 +1,215 @@
|
||||
# Copyright 2016-2019 Camptocamp SA
|
||||
# Copyright 2021 Open Source Integrators
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from openerp.tools.translate import _
|
||||
from openerp.osv import osv
|
||||
from openerp.osv.orm import except_orm
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from azure.storage.blob import (
|
||||
BlobServiceClient,
|
||||
generate_account_sas,
|
||||
ResourceTypes,
|
||||
AccountSasPermissions,
|
||||
)
|
||||
from azure.core.exceptions import ResourceExistsError, HttpResponseError
|
||||
except ImportError:
|
||||
_logger.debug("Cannot 'import azure-storage-blob'.")
|
||||
|
||||
try:
|
||||
from azure.identity import DefaultAzureCredential
|
||||
except ImportError:
|
||||
_logger.debug("Cannot 'import azure-identity'.")
|
||||
|
||||
|
||||
class IrAttachment(osv.osv):
|
||||
_inherit = "ir.attachment"
|
||||
|
||||
def _get_stores(self):
|
||||
l = ["azure"]
|
||||
l += super(IrAttachment, self)._get_stores()
|
||||
return l
|
||||
|
||||
def _get_blob_service_client(self):
|
||||
"""Connect to Azure and return the blob service client
|
||||
|
||||
The following environment variables must be set:
|
||||
* ``AZURE_STORAGE_CONNECTION_STRING``
|
||||
or
|
||||
* ``AZURE_STORAGE_ACCOUNT_NAME``
|
||||
* ``AZURE_STORAGE_ACCOUNT_URL``
|
||||
* ``AZURE_STORAGE_ACCOUNT_KEY``
|
||||
or if you want to use AAD (pod identity), set it to 1 or 0
|
||||
* ``AZURE_STORAGE_USE_AAD``
|
||||
|
||||
"""
|
||||
connect_str = os.environ.get("AZURE_STORAGE_CONNECTION_STRING")
|
||||
account_name = os.environ.get("AZURE_STORAGE_ACCOUNT_NAME")
|
||||
account_url = os.environ.get("AZURE_STORAGE_ACCOUNT_URL")
|
||||
account_key = os.environ.get("AZURE_STORAGE_ACCOUNT_KEY")
|
||||
account_use_aad = os.environ.get("AZURE_STORAGE_USE_AAD")
|
||||
if not (
|
||||
connect_str
|
||||
or (account_name and account_url and account_key)
|
||||
or account_use_aad
|
||||
):
|
||||
msg = _(
|
||||
"If you want to read from the Azure container, you must provide the "
|
||||
"following environment variables:\n"
|
||||
"* AZURE_STORAGE_CONNECTION_STRING\n"
|
||||
"or\n"
|
||||
"* AZURE_STORAGE_ACCOUNT_NAME\n"
|
||||
"* AZURE_STORAGE_ACCOUNT_URL\n"
|
||||
"* AZURE_STORAGE_ACCOUNT_KEY\n"
|
||||
"or\n"
|
||||
"* AZURE_STORAGE_USE_AAD\n"
|
||||
)
|
||||
raise osv.except_osv(_("UserError"), msg)
|
||||
blob_service_client = None
|
||||
if account_use_aad:
|
||||
token_credential = DefaultAzureCredential()
|
||||
blob_service_client = BlobServiceClient(
|
||||
account_url=account_url, credential=token_credential
|
||||
)
|
||||
elif connect_str:
|
||||
try:
|
||||
blob_service_client = BlobServiceClient.from_connection_string(
|
||||
connect_str
|
||||
)
|
||||
except HttpResponseError as error:
|
||||
_logger.exception(
|
||||
"Error during the connection to Azure container using the "
|
||||
"connection string."
|
||||
)
|
||||
raise osv.except_osv(_("UserError"), str(error))
|
||||
else:
|
||||
try:
|
||||
sas_token = generate_account_sas(
|
||||
account_name=account_name,
|
||||
account_key=account_key,
|
||||
resource_types=ResourceTypes(container=True, object=True),
|
||||
permission=AccountSasPermissions(read=True, write=True),
|
||||
expiry=datetime.utcnow() + timedelta(hours=1),
|
||||
)
|
||||
blob_service_client = BlobServiceClient(
|
||||
account_url=account_url,
|
||||
credential=sas_token,
|
||||
)
|
||||
except HttpResponseError as error:
|
||||
_logger.exception(
|
||||
"Error during the connection to Azure container using the Shared "
|
||||
"Access Signature (SAS)"
|
||||
)
|
||||
raise osv.except_osv(_("UserError"), str(error))
|
||||
return blob_service_client
|
||||
|
||||
def _get_container_name(self):
|
||||
"""
|
||||
Container naming rules:
|
||||
https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names
|
||||
"""
|
||||
running_env = os.environ.get("RUNNING_ENV", "dev")
|
||||
dbname = os.environ.get("DB_NAME", "odoodb")
|
||||
storage_name = os.environ.get("AZURE_STORAGE_NAME", r"{env}-{db}")
|
||||
storage_name = storage_name.format(env=running_env, db=dbname)
|
||||
# replace invalid characters by _
|
||||
storage_name = re.sub(r"[\W_]+", "-", storage_name)
|
||||
# lowercase, max 63 chars
|
||||
return str.lower(storage_name)[:63]
|
||||
|
||||
def _get_azure_container(self, container_name=None):
|
||||
if not container_name:
|
||||
container_name = self._get_container_name()
|
||||
try:
|
||||
blob_service_client = self._get_blob_service_client()
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
"error accessing to storage '%s' please check credentials ",
|
||||
container_name,
|
||||
)
|
||||
return False
|
||||
container_client = blob_service_client.get_container_client(container_name)
|
||||
if not container_client.exists():
|
||||
try:
|
||||
# Create the container
|
||||
container_client.create_container()
|
||||
except HttpResponseError as error:
|
||||
_logger.exception("Error during the creation of the Azure container")
|
||||
raise osv.except_osv(_("UserError"), str(error))
|
||||
return container_client
|
||||
|
||||
def _store_file_read(self, fname, bin_size=False):
|
||||
if fname.startswith("azure://"):
|
||||
key = fname.replace("azure://", "", 1).lower()
|
||||
if "/" in key:
|
||||
container_name, key = key.split("/", 1)
|
||||
else:
|
||||
container_name = None
|
||||
container_client = self._get_azure_container(container_name)
|
||||
# if container cannot be retrived, abort reading from azure storage
|
||||
if not container_client:
|
||||
return ""
|
||||
try:
|
||||
blob_client = container_client.get_blob_client(key)
|
||||
read = blob_client.download_blob().readall()
|
||||
except HttpResponseError:
|
||||
read = ""
|
||||
_logger.info("Attachment '%s' missing on object storage", fname)
|
||||
return read
|
||||
else:
|
||||
return super(IrAttachment, self)._store_file_read(fname, bin_size)
|
||||
|
||||
def _store_file_write(self, storage, key, bin_data):
|
||||
if storage == "azure":
|
||||
container_client = self._get_azure_container()
|
||||
filename = "azure://%s/%s" % (container_client.container_name, key)
|
||||
with io.BytesIO() as file:
|
||||
blob_client = container_client.get_blob_client(key.lower())
|
||||
file.write(bin_data)
|
||||
file.seek(0)
|
||||
try:
|
||||
blob_client.upload_blob(file, blob_type="BlockBlob")
|
||||
except ResourceExistsError:
|
||||
pass
|
||||
except HttpResponseError as error:
|
||||
# log verbose error from azure, return short message for user
|
||||
_logger.exception("Error during storage of the file %s" % filename)
|
||||
raise osv.except_osv(
|
||||
_("UserError"),
|
||||
_("The file could not be stored: %s") % str(error),
|
||||
)
|
||||
else:
|
||||
_super = super(IrAttachment, self)
|
||||
filename = _super._store_file_write(key, bin_data)
|
||||
return filename
|
||||
|
||||
def _store_file_delete(self, fname):
|
||||
if fname.startswith("azure://"):
|
||||
key = fname.replace("azure://", "", 1).lower()
|
||||
if "/" in key:
|
||||
container_name, key = key.split("/", 1)
|
||||
else:
|
||||
container_name = None
|
||||
container_client = self._get_azure_container(container_name)
|
||||
if not container_client:
|
||||
return ""
|
||||
# delete the file only if it is on the current configured container
|
||||
# otherwise, we might delete files used on a different environment
|
||||
try:
|
||||
blob_client = container_client.get_blob_client(key)
|
||||
blob_client.delete_blob()
|
||||
_logger.info("File %s deleted on the object storage" % (fname))
|
||||
except HttpResponseError:
|
||||
# log verbose error from azure, return short message for
|
||||
# user
|
||||
_logger.exception("Error during deletion of the file %s" % fname)
|
||||
else:
|
||||
super(IrAttachment, self)._store_file_delete(fname)
|
||||
@@ -20,21 +20,19 @@ _logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def clean_fs(files):
|
||||
_logger.info('cleaning old files from filestore')
|
||||
_logger.info("cleaning old files from filestore")
|
||||
for full_path in files:
|
||||
if os.path.exists(full_path):
|
||||
try:
|
||||
os.unlink(full_path)
|
||||
except OSError:
|
||||
_logger.info(
|
||||
"_file_delete could not unlink %s",
|
||||
full_path, exc_info=True
|
||||
"_file_delete could not unlink %s", full_path, exc_info=True
|
||||
)
|
||||
except IOError:
|
||||
# Harmless and needed for race conditions
|
||||
_logger.info(
|
||||
"_file_delete could not unlink %s",
|
||||
full_path, exc_info=True
|
||||
"_file_delete could not unlink %s", full_path, exc_info=True
|
||||
)
|
||||
|
||||
|
||||
@@ -52,7 +50,7 @@ def savepoint(cursor):
|
||||
|
||||
|
||||
class IrAttachment(osv.osv):
|
||||
_inherit = 'ir.attachment'
|
||||
_inherit = "ir.attachment"
|
||||
|
||||
@staticmethod
|
||||
def _compute_checksum(bin_data):
|
||||
@@ -60,26 +58,24 @@ class IrAttachment(osv.osv):
|
||||
:param bin_data : datas in its binary form
|
||||
"""
|
||||
# an empty file has a checksum too (for caching)
|
||||
return hashlib.sha1(bin_data or '').hexdigest()
|
||||
return hashlib.sha1(bin_data or "").hexdigest()
|
||||
|
||||
def _is_user_admin(self, cr, uid):
|
||||
if uid == SUPERUSER_ID:
|
||||
return True
|
||||
else:
|
||||
return self.pool.get('res.users').has_group(
|
||||
cr, uid, 'base.group_erp_manager'
|
||||
return self.pool.get("res.users").has_group(
|
||||
cr, uid, "base.group_erp_manager"
|
||||
)
|
||||
|
||||
def _storage(self, cr, uid, context=None):
|
||||
return self.pool['ir.config_parameter'].get_param(
|
||||
cr, SUPERUSER_ID, 'ir_attachment.location', 'file'
|
||||
return self.pool["ir.config_parameter"].get_param(
|
||||
cr, SUPERUSER_ID, "ir_attachment.location", "file"
|
||||
)
|
||||
|
||||
def _full_path(self, cr, uid, location, path):
|
||||
# Hack to allow filestore migration from local filesystem to any remote
|
||||
return super(IrAttachment, self)._full_path(
|
||||
cr, uid, 'file://filestore', path
|
||||
)
|
||||
return super(IrAttachment, self)._full_path(cr, uid, "file://filestore", path)
|
||||
|
||||
def _register_hook(self, cr):
|
||||
super(IrAttachment, self)._register_hook(cr)
|
||||
@@ -101,7 +97,7 @@ class IrAttachment(osv.osv):
|
||||
# done during the initialization. We need to move the attachments that
|
||||
# could have been created or updated in other addons before this addon
|
||||
# was loaded
|
||||
update_module = load_modules_frame.f_locals.get('update_module')
|
||||
update_module = load_modules_frame.f_locals.get("update_module")
|
||||
|
||||
# We need to call the migration on the loading of the model because
|
||||
# when we are upgrading addons, some of them might add attachments.
|
||||
@@ -110,7 +106,7 @@ class IrAttachment(osv.osv):
|
||||
# Typical example is images of ir.ui.menu which are updated in
|
||||
# ir.attachment at every upgrade of the addons
|
||||
if update_module:
|
||||
self.pool.get('ir.attachment')._force_storage_to_object_storage(
|
||||
self.pool.get("ir.attachment")._force_storage_to_object_storage(
|
||||
cr, SUPERUSER_ID
|
||||
)
|
||||
|
||||
@@ -130,12 +126,11 @@ class IrAttachment(osv.osv):
|
||||
an old database with attachments pointing to deleted assets.
|
||||
|
||||
"""
|
||||
assert (isinstance(ids, int) or
|
||||
len(ids) == 1), 'Expecting only one record'
|
||||
assert isinstance(ids, int) or len(ids) == 1, "Expecting only one record"
|
||||
rec = self.browse(cr, uid, ids, context=context)
|
||||
|
||||
# assets
|
||||
if rec.res_model == 'ir.ui.view':
|
||||
if rec.res_model == "ir.ui.view":
|
||||
# assets are stored in 'ir.ui.view'
|
||||
return True
|
||||
|
||||
@@ -146,58 +141,50 @@ class IrAttachment(osv.osv):
|
||||
# we keep them in the database instead of the object storage
|
||||
location = self._storage(cr, uid)
|
||||
for attach in self.browse(cr, uid, id, context):
|
||||
if (location in self._get_stores() and
|
||||
self._save_in_db_anyway(cr, uid, [id], context)):
|
||||
if location in self._get_stores() and self._save_in_db_anyway(
|
||||
cr, uid, [id], context
|
||||
):
|
||||
# compute the fields that depend on datas
|
||||
bin_data = value and value.decode('base64') or ''
|
||||
bin_data = value and value.decode("base64") or ""
|
||||
vals = {
|
||||
'file_size': len(bin_data),
|
||||
'checksum': self._compute_checksum(bin_data),
|
||||
'db_datas': value,
|
||||
"file_size": len(bin_data),
|
||||
"checksum": self._compute_checksum(bin_data),
|
||||
"db_datas": value,
|
||||
# we seriously don't need index content on those fields
|
||||
'index_content': False,
|
||||
'store_fname': False,
|
||||
"index_content": False,
|
||||
"store_fname": False,
|
||||
}
|
||||
fname = attach.store_fname
|
||||
# write as superuser, as user probably does not
|
||||
# have write access
|
||||
super(IrAttachment, self).write(
|
||||
cr, SUPERUSER_ID, id, vals, context
|
||||
)
|
||||
super(IrAttachment, self).write(cr, SUPERUSER_ID, id, vals, context)
|
||||
if fname:
|
||||
self._file_delete(cr, uid, fname)
|
||||
continue
|
||||
self._data_set(cr, uid, id, 'datas', value, None, context)
|
||||
self._data_set(cr, uid, id, "datas", value, None, context)
|
||||
|
||||
def _store_file_read(self, fname, bin_size=False):
|
||||
storage = fname.partition('://')[0]
|
||||
raise NotImplementedError(
|
||||
'No implementation for %s' % (storage,)
|
||||
)
|
||||
storage = fname.partition("://")[0]
|
||||
raise NotImplementedError("No implementation for %s" % (storage,))
|
||||
|
||||
def _store_file_write(self, storage, key, bin_data):
|
||||
raise NotImplementedError(
|
||||
'No implementation for %s' % (storage,)
|
||||
)
|
||||
raise NotImplementedError("No implementation for %s" % (storage,))
|
||||
|
||||
def _store_file_delete(self, fname):
|
||||
storage = fname.partition('://')[0]
|
||||
raise NotImplementedError(
|
||||
'No implementation for %s' % (storage,)
|
||||
)
|
||||
storage = fname.partition("://")[0]
|
||||
raise NotImplementedError("No implementation for %s" % (storage,))
|
||||
|
||||
def _file_read(self, cr, uid, location, fname, bin_size=False):
|
||||
if self._is_file_from_a_store(fname):
|
||||
return self._store_file_read(fname, bin_size=bin_size)
|
||||
else:
|
||||
_super = super(IrAttachment, self)
|
||||
return _super._file_read(cr, uid, location,
|
||||
fname, bin_size=bin_size)
|
||||
return _super._file_read(cr, uid, location, fname, bin_size=bin_size)
|
||||
|
||||
def _file_write(self, cr, uid, location, value):
|
||||
storage = self._storage(cr, uid)
|
||||
if storage in self._get_stores():
|
||||
bin_data = value.decode('base64')
|
||||
bin_data = value.decode("base64")
|
||||
key = self._compute_checksum(bin_data)
|
||||
filename = self._store_file_write(storage, key, bin_data)
|
||||
else:
|
||||
@@ -209,8 +196,9 @@ class IrAttachment(osv.osv):
|
||||
if self._is_file_from_a_store(fname):
|
||||
# using SQL to include files hidden through unlink or due to record
|
||||
# rules
|
||||
cr.execute("SELECT COUNT(*) FROM ir_attachment "
|
||||
"WHERE store_fname = %s", (fname,))
|
||||
cr.execute(
|
||||
"SELECT COUNT(*) FROM ir_attachment " "WHERE store_fname = %s", (fname,)
|
||||
)
|
||||
count = cr.fetchone()[0]
|
||||
if int(count) == 1:
|
||||
self._store_file_delete(fname)
|
||||
@@ -219,33 +207,31 @@ class IrAttachment(osv.osv):
|
||||
|
||||
def _is_file_from_a_store(self, fname):
|
||||
for store_name in self._get_stores():
|
||||
uri = '{}://'.format(store_name)
|
||||
uri = "{}://".format(store_name)
|
||||
if fname.startswith(uri):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _move_attachment_to_store(self, cr, uid, ids, context=None):
|
||||
assert (isinstance(ids, int) or
|
||||
len(ids) == 1), 'Expecting only one record'
|
||||
assert isinstance(ids, int) or len(ids) == 1, "Expecting only one record"
|
||||
rec = self.browse(cr, uid, ids, context)
|
||||
_logger.info('inspecting attachment %s (%d)', rec.name, rec.id)
|
||||
_logger.info("inspecting attachment %s (%d)", rec.name, rec.id)
|
||||
fname = rec.store_fname
|
||||
if fname:
|
||||
# migrating from filesystem filestore
|
||||
# or from the old 'store_fname' without the bucket name
|
||||
_logger.info('moving %s on the object storage', fname)
|
||||
self.write(cr, uid, ids, {'datas': rec.datas}, context)
|
||||
_logger.info('moved %s on the object storage', fname)
|
||||
_logger.info("moving %s on the object storage", fname)
|
||||
self.write(cr, uid, ids, {"datas": rec.datas}, context)
|
||||
_logger.info("moved %s on the object storage", fname)
|
||||
return self._full_path(cr, uid, None, fname)
|
||||
elif rec.db_datas:
|
||||
_logger.info('moving on the object storage from database')
|
||||
self.write(cr, uid, ids, {'datas': rec.datas}, context)
|
||||
_logger.info("moving on the object storage from database")
|
||||
self.write(cr, uid, ids, {"datas": rec.datas}, context)
|
||||
|
||||
def force_storage(self, cr, uid, context=None):
|
||||
if not self._is_user_admin(cr, uid):
|
||||
raise except_orm(
|
||||
_('Error'),
|
||||
_('Only administrators can execute this action.')
|
||||
_("Error"), _("Only administrators can execute this action.")
|
||||
)
|
||||
storage = self._storage(cr, uid)
|
||||
if storage not in self._get_stores():
|
||||
@@ -253,10 +239,10 @@ class IrAttachment(osv.osv):
|
||||
self._force_storage_to_object_storage(cr, uid, context)
|
||||
|
||||
def _force_storage_to_object_storage(self, cr, uid, context=None):
|
||||
_logger.info('migrating files to the object storage')
|
||||
_logger.info("migrating files to the object storage")
|
||||
storage = self._storage(cr, uid)
|
||||
|
||||
domain = [('store_fname', 'not like', '{}://%'.format(storage))]
|
||||
domain = [("store_fname", "not like", "{}://%".format(storage))]
|
||||
|
||||
ids = self.search(cr, uid, domain, context=context)
|
||||
files_to_clean = []
|
||||
@@ -273,7 +259,7 @@ class IrAttachment(osv.osv):
|
||||
"WHERE id = %s "
|
||||
"FOR UPDATE NOWAIT",
|
||||
(attachment_id,),
|
||||
log_exceptions=False
|
||||
log_exceptions=False,
|
||||
)
|
||||
|
||||
path = self._move_attachment_to_store(
|
||||
@@ -282,8 +268,9 @@ class IrAttachment(osv.osv):
|
||||
if path:
|
||||
files_to_clean.append(path)
|
||||
except psycopg2.OperationalError:
|
||||
_logger.error('Could not migrate attachment %s to %s' %
|
||||
(attachment_id, storage))
|
||||
_logger.error(
|
||||
"Could not migrate attachment %s to %s" % (attachment_id, storage)
|
||||
)
|
||||
|
||||
def clean():
|
||||
clean_fs(files_to_clean)
|
||||
@@ -291,7 +278,7 @@ class IrAttachment(osv.osv):
|
||||
# delete the files from the filesystem once we know the changes
|
||||
# have been committed in ir.attachment
|
||||
if files_to_clean:
|
||||
cr.after('commit', clean)
|
||||
cr.commit()
|
||||
|
||||
def _get_stores(self):
|
||||
"""To get the list of stores activated in the system"""
|
||||
|
||||
@@ -19,93 +19,95 @@ _logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_true(strval):
|
||||
return bool(strtobool(strval or '0'.lower()))
|
||||
return bool(strtobool(strval or "0".lower()))
|
||||
|
||||
|
||||
PlatformConfig = namedtuple(
|
||||
'PlatformConfig',
|
||||
'filestore'
|
||||
)
|
||||
PlatformConfig = namedtuple("PlatformConfig", "filestore")
|
||||
|
||||
|
||||
class FilestoreKind(object):
|
||||
db = 'db'
|
||||
s3 = 's3' # or compatible s3 object storage
|
||||
swift = 'swift'
|
||||
file = 'file'
|
||||
db = "db"
|
||||
s3 = "s3" # or compatible s3 object storage
|
||||
swift = "swift"
|
||||
file = "file"
|
||||
azure = "azure"
|
||||
|
||||
|
||||
class CloudPlatform(osv.osv_abstract):
|
||||
_name = 'cloud.platform'
|
||||
_name = "cloud.platform"
|
||||
|
||||
def _platform_kinds(self):
|
||||
# XXX for backward compatibility, we need this one here, move
|
||||
# it in cloud_platform_exoscale in V11
|
||||
return ['exoscale']
|
||||
return ["exoscale"]
|
||||
|
||||
def _filestore_kinds(self):
|
||||
# XXX for backward compatibility, we need this one here, move
|
||||
# it in cloud_platform_exoscale in V11
|
||||
return ["exoscale"]
|
||||
|
||||
# XXX for backward compatibility, we need this one here, move
|
||||
# it in cloud_platform_exoscale in V11
|
||||
def _config_by_server_env_for_exoscale(self):
|
||||
configs = {
|
||||
'prod': PlatformConfig(filestore=FilestoreKind.s3),
|
||||
'integration': PlatformConfig(filestore=FilestoreKind.s3),
|
||||
'test': PlatformConfig(filestore=FilestoreKind.db),
|
||||
'dev': PlatformConfig(filestore=FilestoreKind.db),
|
||||
"prod": PlatformConfig(filestore=FilestoreKind.s3),
|
||||
"integration": PlatformConfig(filestore=FilestoreKind.s3),
|
||||
"test": PlatformConfig(filestore=FilestoreKind.db),
|
||||
"dev": PlatformConfig(filestore=FilestoreKind.db),
|
||||
}
|
||||
return configs
|
||||
|
||||
def _config_by_server_env(self, platform_kind, environment):
|
||||
configs_getter = getattr(
|
||||
self,
|
||||
'_config_by_server_env_for_%s' % platform_kind,
|
||||
None
|
||||
self, "_config_by_server_env_for_%s" % platform_kind, None
|
||||
)
|
||||
configs = configs_getter() if configs_getter else {}
|
||||
return configs.get(environment) or FilestoreKind.db
|
||||
|
||||
def _get_running_env(self):
|
||||
environment_name = config['running_env']
|
||||
if environment_name.startswith('labs'):
|
||||
environment_name = config["running_env"]
|
||||
if environment_name.startswith("labs"):
|
||||
# We allow to have environments such as 'labs-logistics'
|
||||
# or 'labs-finance', in order to have the matching ribbon.
|
||||
environment_name = 'labs'
|
||||
environment_name = "labs"
|
||||
return environment_name
|
||||
|
||||
# Due to the addition of the ovh cloud platform
|
||||
# This will be moved to cloud_platform_exoscale on v11
|
||||
def install_exoscale(self, cr, uid, context=None):
|
||||
self.install(cr, uid, 'exoscale', context)
|
||||
self.install(cr, uid, "exoscale", context)
|
||||
|
||||
def install(self, cr, uid, platform_kind, context=None):
|
||||
assert platform_kind in self._platform_kinds()
|
||||
params = self.pool.get('ir.config_parameter')
|
||||
params = self.pool.get("ir.config_parameter")
|
||||
params.set_param(
|
||||
cr, SUPERUSER_ID,
|
||||
'cloud.platform.kind', platform_kind,
|
||||
context=context
|
||||
cr, SUPERUSER_ID, "cloud.platform.kind", platform_kind, context=context
|
||||
)
|
||||
environment_name = self._get_running_env()
|
||||
configs = self._config_by_server_env(platform_kind, environment_name)
|
||||
params.set_param(
|
||||
cr, SUPERUSER_ID,
|
||||
'ir_attachment.location', configs.filestore,
|
||||
context=context
|
||||
cr,
|
||||
SUPERUSER_ID,
|
||||
"ir_attachment.location",
|
||||
configs.filestore,
|
||||
context=context,
|
||||
)
|
||||
self.check(cr, uid, context)
|
||||
if configs.filestore in [FilestoreKind.swift, FilestoreKind.s3]:
|
||||
self.pool.get('ir.attachment').force_storage(
|
||||
self.pool.get("ir.attachment").force_storage(
|
||||
cr, SUPERUSER_ID, context=context
|
||||
)
|
||||
_logger.info('cloud platform configured for {}'.format(platform_kind))
|
||||
_logger.info("cloud platform configured for {}".format(platform_kind))
|
||||
|
||||
def _check_swift(self, cr, uid, environment_name, context=None):
|
||||
params = self.pool.get('ir.config_parameter')
|
||||
params = self.pool.get("ir.config_parameter")
|
||||
use_swift = (
|
||||
params.get_param(
|
||||
cr, SUPERUSER_ID, 'ir_attachment.location', context=context
|
||||
) == FilestoreKind.swift
|
||||
cr, SUPERUSER_ID, "ir_attachment.location", context=context
|
||||
)
|
||||
if environment_name in ('prod', 'integration'):
|
||||
== FilestoreKind.swift
|
||||
)
|
||||
if environment_name in ("prod", "integration"):
|
||||
# Labs instances use swift or s3 by default, but we don't want
|
||||
# to enforce it in case we want to test something with a different
|
||||
# storage. At your own risks!
|
||||
@@ -116,19 +118,19 @@ class CloudPlatform(osv.osv_abstract):
|
||||
"automatically."
|
||||
)
|
||||
if use_swift:
|
||||
assert os.environ.get('SWIFT_AUTH_URL'), (
|
||||
assert os.environ.get("SWIFT_AUTH_URL"), (
|
||||
"SWIFT_AUTH_URL environment variable is required when "
|
||||
"ir_attachment.location is 'swift'."
|
||||
)
|
||||
assert os.environ.get('SWIFT_ACCOUNT'), (
|
||||
assert os.environ.get("SWIFT_ACCOUNT"), (
|
||||
"SWIFT_ACCOUNT environment variable is required when "
|
||||
"ir_attachment.location is 'swift'."
|
||||
)
|
||||
assert os.environ.get('SWIFT_PASSWORD'), (
|
||||
assert os.environ.get("SWIFT_PASSWORD"), (
|
||||
"SWIFT_PASSWORD environment variable is required when "
|
||||
"ir_attachment.location is 'swift'."
|
||||
)
|
||||
container_name = os.environ.get('SWIFT_WRITE_CONTAINER')
|
||||
container_name = os.environ.get("SWIFT_WRITE_CONTAINER")
|
||||
assert container_name, (
|
||||
"SWIFT_WRITE_CONTAINER environment variable is required when "
|
||||
"ir_attachment.location is 'swift'.\n"
|
||||
@@ -139,9 +141,8 @@ class CloudPlatform(osv.osv_abstract):
|
||||
"If you don't actually need a bucket, change the"
|
||||
" 'ir_attachment.location' parameter."
|
||||
)
|
||||
prod_container = bool(re.match(r'[a-z0-9-]+-odoo-prod',
|
||||
container_name))
|
||||
if environment_name == 'prod':
|
||||
prod_container = bool(re.match(r"[a-z0-9-]+-odoo-prod", container_name))
|
||||
if environment_name == "prod":
|
||||
assert prod_container, (
|
||||
"SWIFT_WRITE_CONTAINER should match '<client>-odoo-prod', "
|
||||
"we got: '%s'" % (container_name,)
|
||||
@@ -153,21 +154,28 @@ class CloudPlatform(osv.osv_abstract):
|
||||
"SWIFT_WRITE_CONTAINER should not match "
|
||||
"'<client>-odoo-prod', we got: '%s'" % (container_name,)
|
||||
)
|
||||
elif environment_name == 'test':
|
||||
elif environment_name == "test":
|
||||
# store in DB so we don't have files local to the host
|
||||
assert params.get_param(cr, SUPERUSER_ID, 'ir_attachment.location',
|
||||
context=context) == 'db', (
|
||||
assert (
|
||||
params.get_param(
|
||||
cr, SUPERUSER_ID, "ir_attachment.location", context=context
|
||||
)
|
||||
== "db"
|
||||
), (
|
||||
"In test instances, files must be stored in the database with "
|
||||
"'ir_attachment.location' set to 'db'. This is "
|
||||
"automatically set by the function 'install_ovh()'."
|
||||
)
|
||||
|
||||
def _check_s3(self, cr, uid, environment_name, context=None):
|
||||
params = self.pool.get('ir.config_parameter')
|
||||
use_s3 = params.get_param(
|
||||
cr, SUPERUSER_ID, 'ir_attachment.location', context=context
|
||||
) == FilestoreKind.s3
|
||||
if environment_name in ('prod', 'integration'):
|
||||
params = self.pool.get("ir.config_parameter")
|
||||
use_s3 = (
|
||||
params.get_param(
|
||||
cr, SUPERUSER_ID, "ir_attachment.location", context=context
|
||||
)
|
||||
== FilestoreKind.s3
|
||||
)
|
||||
if environment_name in ("prod", "integration"):
|
||||
# Labs instances use swift or s3 by default, but we don't want
|
||||
# to enforce it in case we want to test something with a different
|
||||
# storage. At your own risks!
|
||||
@@ -178,15 +186,15 @@ class CloudPlatform(osv.osv_abstract):
|
||||
"automatically."
|
||||
)
|
||||
if use_s3:
|
||||
assert os.environ.get('AWS_ACCESS_KEY_ID'), (
|
||||
assert os.environ.get("AWS_ACCESS_KEY_ID"), (
|
||||
"AWS_ACCESS_KEY_ID environment variable is required when "
|
||||
"ir_attachment.location is 's3'."
|
||||
)
|
||||
assert os.environ.get('AWS_SECRET_ACCESS_KEY'), (
|
||||
assert os.environ.get("AWS_SECRET_ACCESS_KEY"), (
|
||||
"AWS_SECRET_ACCESS_KEY environment variable is required when "
|
||||
"ir_attachment.location is 's3'."
|
||||
)
|
||||
bucket_name = os.environ.get('AWS_BUCKETNAME')
|
||||
bucket_name = os.environ.get("AWS_BUCKETNAME")
|
||||
assert bucket_name, (
|
||||
"AWS_BUCKETNAME environment variable is required when "
|
||||
"ir_attachment.location is 's3'.\n"
|
||||
@@ -197,8 +205,8 @@ class CloudPlatform(osv.osv_abstract):
|
||||
"If you don't actually need a bucket, change the"
|
||||
" 'ir_attachment.location' parameter."
|
||||
)
|
||||
prod_bucket = bool(re.match(r'[a-z-0-9]+-odoo-prod', bucket_name))
|
||||
if environment_name == 'prod':
|
||||
prod_bucket = bool(re.match(r"[a-z-0-9]+-odoo-prod", bucket_name))
|
||||
if environment_name == "prod":
|
||||
assert prod_bucket, (
|
||||
"AWS_BUCKETNAME should match '<client>-odoo-prod', "
|
||||
"we got: '%s'" % (bucket_name,)
|
||||
@@ -211,46 +219,137 @@ class CloudPlatform(osv.osv_abstract):
|
||||
"we got: '%s'" % (bucket_name,)
|
||||
)
|
||||
|
||||
elif environment_name == 'test':
|
||||
elif environment_name == "test":
|
||||
# store in DB so we don't have files local to the host
|
||||
assert params.get_param(cr, SUPERUSER_ID, 'ir_attachment.location',
|
||||
context=context) == 'db', (
|
||||
assert (
|
||||
params.get_param(
|
||||
cr, SUPERUSER_ID, "ir_attachment.location", context=context
|
||||
)
|
||||
== "db"
|
||||
), (
|
||||
"In test instances, files must be stored in the database with "
|
||||
"'ir_attachment.location' set to 'db'. This is "
|
||||
"automatically set by the function 'install_exoscale()'."
|
||||
)
|
||||
|
||||
def _check_azure(self, cr, uid, environment_name, context=None):
|
||||
params = self.pool.get("ir.config_parameter")
|
||||
use_azure = (
|
||||
params.get_param(
|
||||
cr, SUPERUSER_ID, "ir_attachment.location", context=context
|
||||
)
|
||||
== FilestoreKind.azure
|
||||
)
|
||||
if environment_name in ("prod", "integration"):
|
||||
# Labs instances use azure by default, but we don't want
|
||||
# to enforce it in case we want to test something with a different
|
||||
# storage. At your own risks!
|
||||
assert use_azure, (
|
||||
"azure must be used on production and integration instances. "
|
||||
"It is activated by setting 'ir_attachment.location.' to 'azure'."
|
||||
" The 'install()' function sets this option "
|
||||
"automatically."
|
||||
)
|
||||
if use_azure:
|
||||
key_sets = [
|
||||
["AZURE_STORAGE_USE_AAD", "AZURE_STORAGE_ACCOUNT_URL"],
|
||||
["AZURE_STORAGE_CONNECTION_STRING"],
|
||||
[
|
||||
"AZURE_STORAGE_ACCOUNT_NAME",
|
||||
"AZURE_STORAGE_ACCOUNT_URL",
|
||||
"AZURE_STORAGE_ACCOUNT_KEY",
|
||||
],
|
||||
]
|
||||
is_valid = False
|
||||
for key_set in key_sets:
|
||||
if all([os.environ.get(key) for key in key_set]):
|
||||
is_valid = True
|
||||
break
|
||||
assert is_valid, (
|
||||
"When ir_attachment.location is set to 'azure', "
|
||||
"at least one of the following enviromnent variable set "
|
||||
"is required : {}".format(
|
||||
" or ".join(
|
||||
[" + ".join([key for key in key_set]) for key_set in key_sets]
|
||||
)
|
||||
)
|
||||
)
|
||||
storage_name = os.environ.get("AZURE_STORAGE_NAME", "")
|
||||
if environment_name in ("prod", "integration", "labs"):
|
||||
assert storage_name, (
|
||||
"AZURE_STORAGE_NAME environment variable is required when "
|
||||
"ir_attachment.location is 'azure'.\n"
|
||||
"Normally, 'azure' is activated on labs, integration "
|
||||
"and production, but should not be used in dev environment"
|
||||
" (or using a dedicated dev bucket, never using the "
|
||||
"integration/prod bucket).\n"
|
||||
"If you don't actually need a bucket, change the"
|
||||
" 'ir_attachment.location' parameter."
|
||||
)
|
||||
# A bucket name is defined under the following format
|
||||
# ^[a-z]+\-[a-z]+\-\d+$
|
||||
# Anything other than prod bucket must be suffixed with env name
|
||||
#
|
||||
# Use AZURE_STORAGE_NAME_UNSTRUCTURED to by-pass check
|
||||
# on bucket name structure
|
||||
if os.environ.get("AZURE_STORAGE_NAME_UNSTRUCTURED"):
|
||||
return
|
||||
prod_bucket = bool(re.match(r"^[a-z]+\-[a-z]+\-\d+$", storage_name))
|
||||
if environment_name == "prod":
|
||||
assert prod_bucket, (
|
||||
"AZURE_STORAGE_NAME should match '^[a-z]+\\-[a-z]+\\-\\d+$', "
|
||||
"we got: '%s'" % (storage_name,)
|
||||
)
|
||||
else:
|
||||
# if we are using the prod bucket on another instance
|
||||
# such as an integration, we must be sure to be in read only!
|
||||
assert not prod_bucket, (
|
||||
"AZURE_STORAGE_NAME should not match '^[a-z]+\\-[a-z]+\\-\\d+$', "
|
||||
"we got: '%s'" % (storage_name,)
|
||||
)
|
||||
|
||||
elif environment_name == "test":
|
||||
# store in DB so we don't have files local to the host
|
||||
assert (
|
||||
params.get_param(
|
||||
cr, SUPERUSER_ID, "ir_attachment.location", context=context
|
||||
)
|
||||
== "db"
|
||||
), (
|
||||
"In test instances, files must be stored in the database with "
|
||||
"'ir_attachment.location' set to 'db'. This is "
|
||||
"automatically set by the function 'install()'."
|
||||
)
|
||||
|
||||
def _check_redis(self, cr, uid, environment_name, context=None):
|
||||
if environment_name in ('prod', 'integration', 'labs', 'test'):
|
||||
assert is_true(os.environ.get('ODOO_SESSION_REDIS')), (
|
||||
if environment_name in ("prod", "integration", "labs", "test"):
|
||||
assert is_true(os.environ.get("ODOO_SESSION_REDIS")), (
|
||||
"Redis must be activated on prod, integration, labs,"
|
||||
" test instances. This is done by setting ODOO_SESSION_REDIS=1."
|
||||
)
|
||||
assert (os.environ.get('ODOO_SESSION_REDIS_HOST') or
|
||||
os.environ.get('ODOO_SESSION_REDIS_SENTINEL_HOST')), (
|
||||
"ODOO_SESSION_REDIS_HOST or ODOO_SESSION_REDIS_SENTINEL_HOST "
|
||||
assert os.environ.get("ODOO_SESSION_REDIS_URL") or os.environ.get(
|
||||
"ODOO_SESSION_REDIS_SENTINEL_URL"
|
||||
), (
|
||||
"ODOO_SESSION_REDIS_URL or ODOO_SESSION_REDIS_SENTINEL_URL "
|
||||
"environment variable is required to connect on Redis"
|
||||
)
|
||||
assert os.environ.get('ODOO_SESSION_REDIS_PREFIX'), (
|
||||
assert os.environ.get("ODOO_SESSION_REDIS_PREFIX"), (
|
||||
"ODOO_SESSION_REDIS_PREFIX environment variable is required "
|
||||
"to store sessions on Redis"
|
||||
)
|
||||
|
||||
prefix = os.environ['ODOO_SESSION_REDIS_PREFIX']
|
||||
assert re.match(r'^[a-z-0-9]+-odoo-[a-z-0-9]+$', prefix), (
|
||||
prefix = os.environ["ODOO_SESSION_REDIS_PREFIX"]
|
||||
assert re.match(r"^[a-z-0-9]+-odoo-[a-z-0-9]+$", prefix), (
|
||||
"ODOO_SESSION_REDIS_PREFIX must match '<client>-odoo-<env>'"
|
||||
", we got: '%s'" % (prefix,)
|
||||
)
|
||||
|
||||
def check(self, cr, uid, context=None):
|
||||
if is_true(os.environ.get('ODOO_CLOUD_PLATFORM_UNSAFE')):
|
||||
_logger.warning(
|
||||
"cloud platform checks disabled, this is not safe"
|
||||
)
|
||||
if is_true(os.environ.get("ODOO_CLOUD_PLATFORM_UNSAFE")):
|
||||
_logger.warning("cloud platform checks disabled, this is not safe")
|
||||
return
|
||||
params = self.pool.get('ir.config_parameter')
|
||||
kind = params.get_param(cr, SUPERUSER_ID,
|
||||
'cloud.platform.kind', context=None)
|
||||
params = self.pool.get("ir.config_parameter")
|
||||
kind = params.get_param(cr, SUPERUSER_ID, "cloud.platform.kind", context=None)
|
||||
if not kind:
|
||||
_logger.warning(
|
||||
"cloud platform not configured, you should "
|
||||
@@ -258,12 +357,14 @@ class CloudPlatform(osv.osv_abstract):
|
||||
)
|
||||
return
|
||||
environment_name = self._get_running_env()
|
||||
if kind == 'exoscale':
|
||||
if kind == "exoscale":
|
||||
self._check_s3(cr, uid, environment_name, context)
|
||||
elif kind == 'ovh':
|
||||
elif kind == "ovh":
|
||||
self._check_swift(cr, uid, environment_name, context)
|
||||
elif kind == "azure":
|
||||
self._check_azure(cr, uid, environment_name, context)
|
||||
self._check_redis(cr, uid, environment_name, context)
|
||||
|
||||
def _register_hook(self, cr):
|
||||
super(CloudPlatform, self)._register_hook(cr)
|
||||
self.pool.get('cloud.platform').check(cr, SUPERUSER_ID)
|
||||
self.pool.get("cloud.platform").check(cr, SUPERUSER_ID)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Cloud Platform Azure
|
||||
====================
|
||||
|
||||
Install addons specific to the Azure setup.
|
||||
|
||||
* The object storage is Azure blob storage
|
||||
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright 2017-2021 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
|
||||
{
|
||||
"name": "Cloud Platform Azure",
|
||||
"summary": "Addons required for the Camptocamp Cloud Platform on Azure",
|
||||
"version": "15.0.1.0.0",
|
||||
"author": "Camptocamp,Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"category": "Extra Tools",
|
||||
"depends": [
|
||||
"cloud_platform",
|
||||
"attachment_azure",
|
||||
],
|
||||
"excludes": [
|
||||
"cloud_platform_ovh",
|
||||
"cloud_platform_exoscale",
|
||||
],
|
||||
"website": "https://www.camptocamp.com",
|
||||
"data": [],
|
||||
"installable": True,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
from . import cloud_platform
|
||||
@@ -0,0 +1,37 @@
|
||||
# Copyright 2016-2021 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
import re
|
||||
import os
|
||||
|
||||
from openerp.osv import osv
|
||||
from openerp.addons.cloud_platform.models.cloud_platform import FilestoreKind
|
||||
from openerp.addons.cloud_platform.models.cloud_platform import PlatformConfig
|
||||
|
||||
|
||||
class CloudPlatform(osv.osv):
|
||||
_inherit = "cloud.platform"
|
||||
|
||||
def _filestore_kinds(self):
|
||||
kinds = super(CloudPlatform, self)._filestore_kinds()
|
||||
kinds.append("azure")
|
||||
return kinds
|
||||
|
||||
def _platform_kinds(self):
|
||||
kinds = super(CloudPlatform, self)._platform_kinds()
|
||||
kinds.append("azure")
|
||||
return kinds
|
||||
|
||||
def _config_by_server_env_for_azure(self):
|
||||
fs_kinds = self._filestore_kinds()
|
||||
configs = {
|
||||
"prod": PlatformConfig(filestore=fs_kinds["azure"]),
|
||||
"integration": PlatformConfig(filestore=fs_kinds["azure"]),
|
||||
"labs": PlatformConfig(filestore=fs_kinds["azure"]),
|
||||
"test": PlatformConfig(filestore=fs_kinds["db"]),
|
||||
"dev": PlatformConfig(filestore=fs_kinds["db"]),
|
||||
}
|
||||
return configs
|
||||
|
||||
def install_azure(self, cr, uid, context=None):
|
||||
self.install(cr, uid, "azure", context)
|
||||
+31
-23
@@ -24,41 +24,41 @@ except ImportError:
|
||||
|
||||
|
||||
def is_true(strval):
|
||||
return bool(strtobool(strval or '0'.lower()))
|
||||
return bool(strtobool(strval or "0".lower()))
|
||||
|
||||
|
||||
sentinel_host = os.environ.get('ODOO_SESSION_REDIS_SENTINEL_HOST')
|
||||
sentinel_master_name = os.environ.get(
|
||||
'ODOO_SESSION_REDIS_SENTINEL_MASTER_NAME'
|
||||
)
|
||||
sentinel_host = os.environ.get("ODOO_SESSION_REDIS_SENTINEL_HOST")
|
||||
sentinel_master_name = os.environ.get("ODOO_SESSION_REDIS_SENTINEL_MASTER_NAME")
|
||||
if sentinel_host and not sentinel_master_name:
|
||||
raise Exception(
|
||||
"ODOO_SESSION_REDIS_SENTINEL_MASTER_NAME must be defined "
|
||||
"when using session_redis"
|
||||
)
|
||||
sentinel_port = int(os.environ.get('ODOO_SESSION_REDIS_SENTINEL_PORT', 26379))
|
||||
host = os.environ.get('ODOO_SESSION_REDIS_HOST', 'localhost')
|
||||
port = int(os.environ.get('ODOO_SESSION_REDIS_PORT', 6379))
|
||||
prefix = os.environ.get('ODOO_SESSION_REDIS_PREFIX')
|
||||
url = os.environ.get('ODOO_SESSION_REDIS_URL')
|
||||
password = os.environ.get('ODOO_SESSION_REDIS_PASSWORD')
|
||||
expiration = os.environ.get('ODOO_SESSION_REDIS_EXPIRATION')
|
||||
anon_expiration = os.environ.get('ODOO_SESSION_REDIS_EXPIRATION_ANONYMOUS')
|
||||
sentinel_port = int(os.environ.get("ODOO_SESSION_REDIS_SENTINEL_PORT", 26379))
|
||||
host = os.environ.get("ODOO_SESSION_REDIS_URL", "localhost")
|
||||
port = int(os.environ.get("ODOO_SESSION_REDIS_PORT", 6379))
|
||||
prefix = os.environ.get("ODOO_SESSION_REDIS_PREFIX")
|
||||
url = os.environ.get("ODOO_SESSION_REDIS_URL")
|
||||
password = os.environ.get("ODOO_SESSION_REDIS_PASSWORD")
|
||||
expiration = os.environ.get("ODOO_SESSION_REDIS_EXPIRATION")
|
||||
anon_expiration = os.environ.get("ODOO_SESSION_REDIS_EXPIRATION_ANONYMOUS")
|
||||
|
||||
|
||||
def session_store():
|
||||
if sentinel_host:
|
||||
sentinel = Sentinel([(sentinel_host, sentinel_port)],
|
||||
password=password)
|
||||
sentinel = Sentinel([(sentinel_host, sentinel_port)], password=password)
|
||||
redis_client = sentinel.master_for(sentinel_master_name)
|
||||
elif url:
|
||||
redis_client = redis.from_url(url)
|
||||
else:
|
||||
redis_client = redis.Redis(host=host, port=port, password=password)
|
||||
return RedisSessionStore(redis=redis_client, prefix=prefix,
|
||||
return RedisSessionStore(
|
||||
redis=redis_client,
|
||||
prefix=prefix,
|
||||
expiration=expiration,
|
||||
anon_expiration=anon_expiration,
|
||||
session_class=Session)
|
||||
session_class=Session,
|
||||
)
|
||||
|
||||
|
||||
def session_gc(session_store):
|
||||
@@ -79,18 +79,26 @@ def purge_fs_sessions(path):
|
||||
pass
|
||||
|
||||
|
||||
if is_true(os.environ.get('ODOO_SESSION_REDIS')):
|
||||
if is_true(os.environ.get("ODOO_SESSION_REDIS")):
|
||||
if sentinel_host:
|
||||
_logger.debug("HTTP sessions stored in Redis with prefix '%s'. "
|
||||
_logger.debug(
|
||||
"HTTP sessions stored in Redis with prefix '%s'. "
|
||||
"Using Sentinel on %s:%s",
|
||||
sentinel_host, sentinel_port, prefix or '')
|
||||
sentinel_host,
|
||||
sentinel_port,
|
||||
prefix or "",
|
||||
)
|
||||
else:
|
||||
_logger.debug("HTTP sessions stored in Redis with prefix '%s' on "
|
||||
"%s:%s", host, port, prefix or '')
|
||||
_logger.debug(
|
||||
"HTTP sessions stored in Redis with prefix '%s' on " "%s:%s",
|
||||
host,
|
||||
port,
|
||||
prefix or "",
|
||||
)
|
||||
|
||||
store = session_store()
|
||||
for handler in openerp.service.wsgi_server.module_handlers:
|
||||
if hasattr(handler, 'session_store'):
|
||||
if hasattr(handler, "session_store"):
|
||||
handler.session_store = store
|
||||
http.session_gc = session_gc
|
||||
# clean the existing sessions on the file system
|
||||
|
||||
Reference in New Issue
Block a user