mirror of
https://github.com/camptocamp/odoo-cloud-platform.git
synced 2026-06-23 18:04:34 +00:00
feat: remove unmaintened modules (#443)
This commit is contained in:
co-authored by
GitHub
parent
25c7d71eb4
commit
b1434e34d0
@@ -1,58 +0,0 @@
|
|||||||
Attachments on S3 storage
|
|
||||||
=========================
|
|
||||||
|
|
||||||
This addon allows to store the attachments (documents and assets) on S3 or any
|
|
||||||
other S3-compatible Object Storage.
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Activate S3 storage:
|
|
||||||
|
|
||||||
* Create or set the system parameter with the key ``ir_attachment.location``
|
|
||||||
and the value in the form ``s3``.
|
|
||||||
|
|
||||||
Configure accesses with environment variables:
|
|
||||||
|
|
||||||
* ``AWS_HOST`` (not required if using AWS services)
|
|
||||||
* ``AWS_REGION`` (required if using AWS services)
|
|
||||||
* ``AWS_ACCESS_KEY_ID``
|
|
||||||
* ``AWS_SECRET_ACCESS_KEY``
|
|
||||||
* ``AWS_BUCKETNAME`` (optional {db} placeholder)
|
|
||||||
|
|
||||||
Read-only mode:
|
|
||||||
|
|
||||||
The bucket and the file key are stored in the attachment. So if you change the
|
|
||||||
``AWS_BUCKETNAME`` or the ``ir_attachment.location``, the existing attachments
|
|
||||||
will still be read on their former bucket. But as soon as they are written over
|
|
||||||
or new attachments are created, they will be created on the new bucket or on
|
|
||||||
the other location (db or filesystem). This is a convenient way to be able to
|
|
||||||
read the production attachments on a replication (since you have the
|
|
||||||
credentials) without any risk to alter the production data.
|
|
||||||
|
|
||||||
This addon must be added in the server wide addons with (``--load`` option):
|
|
||||||
|
|
||||||
``--load=web,attachment_s3``
|
|
||||||
|
|
||||||
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``.
|
|
||||||
|
|
||||||
Multi-tenancy
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Use the `{db}` placeholder to handle multi-tenancy.
|
|
||||||
|
|
||||||
On instances that hold multiple databases, it's preferable to have one bucket per database.
|
|
||||||
|
|
||||||
To handle this, you can insert the `{db}` placeholder in your bucket name variable ``AWS_BUCKETNAME``.
|
|
||||||
It will be replaced by the database name.
|
|
||||||
This will give you a unique bucketname per database.
|
|
||||||
|
|
||||||
|
|
||||||
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 S3.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import models
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Copyright 2016-2021 Camptocamp SA
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Attachments on S3 storage",
|
|
||||||
"summary": "Store assets and attachments on a S3 compatible object storage",
|
|
||||||
"version": "15.0.1.0.0",
|
|
||||||
"author": "Camptocamp,Odoo Community Association (OCA)",
|
|
||||||
"license": "AGPL-3",
|
|
||||||
"category": "Knowledge Management",
|
|
||||||
"depends": ["base", "base_attachment_object_storage"],
|
|
||||||
"external_dependencies": {
|
|
||||||
"python": ["boto3"],
|
|
||||||
},
|
|
||||||
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
|
||||||
"data": [],
|
|
||||||
"installable": False,
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import ir_attachment
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
# Copyright 2016-2019 Camptocamp SA
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
|
||||||
|
|
||||||
|
|
||||||
import io
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from urllib.parse import urlsplit
|
|
||||||
|
|
||||||
from odoo import _, api, exceptions, models
|
|
||||||
|
|
||||||
from ..s3uri import S3Uri
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
try:
|
|
||||||
import boto3
|
|
||||||
from botocore.exceptions import ClientError, EndpointConnectionError
|
|
||||||
except ImportError:
|
|
||||||
boto3 = None # noqa
|
|
||||||
ClientError = None # noqa
|
|
||||||
EndpointConnectionError = None # noqa
|
|
||||||
_logger.debug("Cannot 'import boto3'.")
|
|
||||||
|
|
||||||
|
|
||||||
class IrAttachment(models.Model):
|
|
||||||
_inherit = "ir.attachment"
|
|
||||||
|
|
||||||
def _get_stores(self):
|
|
||||||
return ["s3"] + super()._get_stores()
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _get_s3_bucket(self, name=None):
|
|
||||||
"""Connect to S3 and return the bucket
|
|
||||||
|
|
||||||
The following environment variables can be set:
|
|
||||||
* ``AWS_HOST``
|
|
||||||
* ``AWS_REGION``
|
|
||||||
* ``AWS_ACCESS_KEY_ID``
|
|
||||||
* ``AWS_SECRET_ACCESS_KEY``
|
|
||||||
* ``AWS_BUCKETNAME``
|
|
||||||
|
|
||||||
If a name is provided, we'll read this bucket, otherwise, the bucket
|
|
||||||
from the environment variable ``AWS_BUCKETNAME`` will be read.
|
|
||||||
|
|
||||||
"""
|
|
||||||
host = os.environ.get("AWS_HOST")
|
|
||||||
|
|
||||||
# Ensure host is prefixed with a scheme (use https as default)
|
|
||||||
if host and not urlsplit(host).scheme:
|
|
||||||
host = "https://%s" % host
|
|
||||||
|
|
||||||
region_name = os.environ.get("AWS_REGION")
|
|
||||||
access_key = os.environ.get("AWS_ACCESS_KEY_ID")
|
|
||||||
secret_key = os.environ.get("AWS_SECRET_ACCESS_KEY")
|
|
||||||
bucket_name = name or os.environ.get("AWS_BUCKETNAME")
|
|
||||||
# replaces {db} by the database name to handle multi-tenancy
|
|
||||||
bucket_name = bucket_name.format(db=self.env.cr.dbname)
|
|
||||||
|
|
||||||
params = {
|
|
||||||
"aws_access_key_id": access_key,
|
|
||||||
"aws_secret_access_key": secret_key,
|
|
||||||
}
|
|
||||||
if host:
|
|
||||||
params["endpoint_url"] = host
|
|
||||||
if region_name:
|
|
||||||
params["region_name"] = region_name
|
|
||||||
if not (access_key and secret_key and bucket_name):
|
|
||||||
msg = _(
|
|
||||||
"If you want to read from the %(bucket_name)s S3 bucket, the following "
|
|
||||||
"environment variables must be set:\n"
|
|
||||||
"* AWS_ACCESS_KEY_ID\n"
|
|
||||||
"* AWS_SECRET_ACCESS_KEY\n"
|
|
||||||
"If you want to write in the %(bucket_name)s S3 bucket, this variable "
|
|
||||||
"must be set as well:\n"
|
|
||||||
"* AWS_BUCKETNAME\n"
|
|
||||||
"Optionally, the S3 host can be changed with:\n"
|
|
||||||
"* AWS_HOST\n"
|
|
||||||
).format(bucket_name=bucket_name)
|
|
||||||
|
|
||||||
raise exceptions.UserError(msg)
|
|
||||||
# try:
|
|
||||||
s3 = boto3.resource("s3", **params)
|
|
||||||
bucket = s3.Bucket(bucket_name)
|
|
||||||
exists = True
|
|
||||||
try:
|
|
||||||
s3.meta.client.head_bucket(Bucket=bucket_name)
|
|
||||||
except ClientError as e:
|
|
||||||
# If a client error is thrown, then check that it was a 404 error.
|
|
||||||
# If it was a 404 error, then the bucket does not exist.
|
|
||||||
error_code = e.response["Error"]["Code"]
|
|
||||||
if error_code == "404":
|
|
||||||
exists = False
|
|
||||||
except EndpointConnectionError as error:
|
|
||||||
# log verbose error from s3, return short message for user
|
|
||||||
msg = _logger.exception("Error during connection on S3")
|
|
||||||
raise exceptions.UserError(str(error)) from None
|
|
||||||
|
|
||||||
if not exists:
|
|
||||||
if not region_name:
|
|
||||||
bucket = s3.create_bucket(Bucket=bucket_name)
|
|
||||||
else:
|
|
||||||
bucket = s3.create_bucket(
|
|
||||||
Bucket=bucket_name,
|
|
||||||
CreateBucketConfiguration={"LocationConstraint": region_name},
|
|
||||||
)
|
|
||||||
return bucket
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _store_file_read(self, fname):
|
|
||||||
if fname.startswith("s3://"):
|
|
||||||
s3uri = S3Uri(fname)
|
|
||||||
try:
|
|
||||||
bucket = self._get_s3_bucket(name=s3uri.bucket())
|
|
||||||
except exceptions.UserError:
|
|
||||||
_logger.exception(
|
|
||||||
"error reading attachment '%s' from object storage", fname
|
|
||||||
)
|
|
||||||
return ""
|
|
||||||
try:
|
|
||||||
key = s3uri.item()
|
|
||||||
bucket.meta.client.head_object(Bucket=bucket.name, Key=key)
|
|
||||||
with io.BytesIO() as res:
|
|
||||||
bucket.download_fileobj(key, res)
|
|
||||||
res.seek(0)
|
|
||||||
read = res.read()
|
|
||||||
except ClientError:
|
|
||||||
read = ""
|
|
||||||
_logger.info("attachment '%s' missing on object storage", fname)
|
|
||||||
return read
|
|
||||||
else:
|
|
||||||
return super()._store_file_read(fname)
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _store_file_write(self, key, bin_data):
|
|
||||||
location = self.env.context.get("storage_location") or self._storage()
|
|
||||||
if location == "s3":
|
|
||||||
bucket = self._get_s3_bucket()
|
|
||||||
obj = bucket.Object(key=key)
|
|
||||||
with io.BytesIO() as file:
|
|
||||||
file.write(bin_data)
|
|
||||||
file.seek(0)
|
|
||||||
filename = "s3://%s/%s" % (bucket.name, key)
|
|
||||||
try:
|
|
||||||
obj.upload_fileobj(file)
|
|
||||||
except ClientError as error:
|
|
||||||
# log verbose error from s3, return short message for user
|
|
||||||
_logger.exception("Error during storage of the file %s" % filename)
|
|
||||||
raise exceptions.UserError(
|
|
||||||
_("The file could not be stored: %s") % str(error)
|
|
||||||
) from None
|
|
||||||
else:
|
|
||||||
_super = super()
|
|
||||||
filename = _super._store_file_write(key, bin_data)
|
|
||||||
return filename
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _store_file_delete(self, fname):
|
|
||||||
if fname.startswith("s3://"):
|
|
||||||
s3uri = S3Uri(fname)
|
|
||||||
bucket_name = s3uri.bucket()
|
|
||||||
item_name = s3uri.item()
|
|
||||||
# delete the file only if it is on the current configured bucket
|
|
||||||
# otherwise, we might delete files used on a different environment
|
|
||||||
if bucket_name == os.environ.get("AWS_BUCKETNAME"):
|
|
||||||
bucket = self._get_s3_bucket()
|
|
||||||
obj = bucket.Object(key=item_name)
|
|
||||||
try:
|
|
||||||
bucket.meta.client.head_object(Bucket=bucket.name, Key=item_name)
|
|
||||||
obj.delete()
|
|
||||||
_logger.info("file %s deleted on the object storage" % (fname,))
|
|
||||||
except ClientError:
|
|
||||||
# log verbose error from s3, return short message for
|
|
||||||
# user
|
|
||||||
_logger.exception("Error during deletion of the file %s" % fname)
|
|
||||||
else:
|
|
||||||
return super()._store_file_delete(fname)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Copyright 2016-2019 Camptocamp SA
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class S3Uri(object):
|
|
||||||
|
|
||||||
_url_re = re.compile("^s3:///*([^/]*)/?(.*)", re.IGNORECASE | re.UNICODE)
|
|
||||||
|
|
||||||
def __init__(self, uri):
|
|
||||||
match = self._url_re.match(uri)
|
|
||||||
if not match:
|
|
||||||
raise ValueError("%s: is not a valid S3 URI" % (uri,))
|
|
||||||
self._bucket, self._item = match.groups()
|
|
||||||
|
|
||||||
def bucket(self):
|
|
||||||
return self._bucket
|
|
||||||
|
|
||||||
def item(self):
|
|
||||||
return self._item
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
Attachments on Swift storage
|
|
||||||
============================
|
|
||||||
|
|
||||||
This addon enable storing attachments (documents and assets) on OpenStack Object Storage (Swift)
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Activate Swift storage:
|
|
||||||
|
|
||||||
* Create or set the system parameter with the key ``ir_attachment.location`` with the following value ``swift``.
|
|
||||||
|
|
||||||
Configure accesses with environment variables:
|
|
||||||
|
|
||||||
* ``SWIFT_AUTH_URL`` : URL of the Swift server
|
|
||||||
* ``SWIFT_TENANT_NAME`` : **!** DEPRECATED **!** Use ``SWIFT_PROJECT_NAME`` instead
|
|
||||||
* ``SWIFT_PROJECT_NAME``
|
|
||||||
* ``SWIFT_ACCOUNT``
|
|
||||||
* ``SWIFT_PASSWORD``
|
|
||||||
* ``SWIFT_REGION_NAME`` : optional region
|
|
||||||
* ``SWIFT_WRITE_CONTAINER`` : Name of the container to use in the store (created if not existing)
|
|
||||||
|
|
||||||
Read-only mode:
|
|
||||||
|
|
||||||
The container name and the key are stored in the attachment. So if you change the
|
|
||||||
``SWIFT_WRITE_CONTAINER`` or the ``ir_attachment.location``, the existing attachments
|
|
||||||
will still be read on their former container. But as soon as they are written over
|
|
||||||
or new attachments are created, they will be created on the new container or on
|
|
||||||
the other location (db or filesystem). This is a convenient way to be able to
|
|
||||||
read the production attachments on a replication (since you have the
|
|
||||||
credentials) without any risk to alter the production data.
|
|
||||||
|
|
||||||
This addon must be added in the server wide addons with (``--load`` option):
|
|
||||||
|
|
||||||
``--load=web,attachment_swift``
|
|
||||||
|
|
||||||
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``.
|
|
||||||
|
|
||||||
Python Dependencies
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
This module needs the python-swiftclient and the python-keystoneclient (For auth v3.0) to work.
|
|
||||||
The python-keystoneclient needs the linux package build-essential and python-dev to install properly.
|
|
||||||
|
|
||||||
The python-swiftclient can be used from the command line, useful to test:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
export AUTH_VERSION=3.0
|
|
||||||
export OS_USERNAME={SWIFT_ACCOUNT}
|
|
||||||
export OS_PASSWORD={SWIFT_PASSWORD}
|
|
||||||
export OS_PROJECT_NAME={SWIFT_PROJECT_NAME}
|
|
||||||
export OS_REGION_NAME={SWIFT_REGION_NAME}
|
|
||||||
export OS_AUTH_URL=https://auth.cloud.ovh.net/v3
|
|
||||||
swift stat
|
|
||||||
|
|
||||||
More information at
|
|
||||||
https://docs.openstack.org/python-swiftclient/latest/cli/index.html#swift-usage
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import models
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Copyright 2017-2021 Camptocamp SA
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Attachments on Swift storage",
|
|
||||||
"summary": "Store assets and attachments on a Swift compatible object store",
|
|
||||||
"version": "15.0.1.0.0",
|
|
||||||
"author": "Camptocamp,Odoo Community Association (OCA)",
|
|
||||||
"license": "AGPL-3",
|
|
||||||
"category": "Knowledge Management",
|
|
||||||
"depends": ["base_attachment_object_storage"],
|
|
||||||
"external_dependencies": {
|
|
||||||
"python": [
|
|
||||||
"swiftclient",
|
|
||||||
"keystoneclient",
|
|
||||||
"keystoneauth1",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
|
||||||
"data": [],
|
|
||||||
"installable": False,
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import ir_attachment
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
# Copyright 2017-2019 Camptocamp SA
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
|
||||||
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
from odoo import _, api, exceptions, models
|
|
||||||
|
|
||||||
from ..swift_uri import SwiftUri
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
try:
|
|
||||||
import keystoneauth1
|
|
||||||
import keystoneauth1.identity
|
|
||||||
import keystoneauth1.session
|
|
||||||
import swiftclient
|
|
||||||
from swiftclient.exceptions import ClientException
|
|
||||||
except ImportError:
|
|
||||||
swiftclient = None
|
|
||||||
ClientException = None
|
|
||||||
_logger.debug("Cannot 'import swiftclient'.")
|
|
||||||
|
|
||||||
|
|
||||||
SWIFT_TIMEOUT = 15
|
|
||||||
|
|
||||||
|
|
||||||
class SwiftSessionStore(object):
|
|
||||||
"""Keep in memory the current Swift Auth session
|
|
||||||
|
|
||||||
The auth endpoint has a rate limit on swift, if every operation
|
|
||||||
on the filestore authenticate, the limit is exhausted and
|
|
||||||
operations rejected with an HTTP error code 429.
|
|
||||||
|
|
||||||
Swift connections can reuse the same session by asking a session
|
|
||||||
matching their connection parameters with ``get_session``.
|
|
||||||
|
|
||||||
The keystoneauth1's session automatically creates a new token
|
|
||||||
if the previous one is expired.
|
|
||||||
|
|
||||||
The best documentation I found about sessions is
|
|
||||||
https://docs.openstack.org/keystoneauth/latest/using-sessions.html
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._sessions = {}
|
|
||||||
|
|
||||||
def _get_key(self, auth_url, username, password, project_name):
|
|
||||||
return (auth_url, username, password, project_name)
|
|
||||||
|
|
||||||
def get_session(
|
|
||||||
self, auth_url=None, username=None, password=None, project_name=None
|
|
||||||
):
|
|
||||||
key = self._get_key(auth_url, username, password, project_name)
|
|
||||||
session = self._sessions.get(key)
|
|
||||||
if not session:
|
|
||||||
auth = keystoneauth1.identity.v3.Password(
|
|
||||||
username=username,
|
|
||||||
password=password,
|
|
||||||
project_name=project_name,
|
|
||||||
auth_url=auth_url,
|
|
||||||
project_domain_id="default",
|
|
||||||
user_domain_id="default",
|
|
||||||
)
|
|
||||||
session = keystoneauth1.session.Session(
|
|
||||||
auth=auth,
|
|
||||||
timeout=SWIFT_TIMEOUT,
|
|
||||||
)
|
|
||||||
self._sessions[key] = session
|
|
||||||
return session
|
|
||||||
|
|
||||||
|
|
||||||
swift_session_store = SwiftSessionStore()
|
|
||||||
|
|
||||||
|
|
||||||
class IrAttachment(models.Model):
|
|
||||||
_inherit = "ir.attachment"
|
|
||||||
|
|
||||||
def _get_stores(self):
|
|
||||||
return ["swift"] + super()._get_stores()
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _get_swift_connection(self):
|
|
||||||
"""Returns a connection object for the Swift object store"""
|
|
||||||
host = os.environ.get("SWIFT_AUTH_URL")
|
|
||||||
account = os.environ.get("SWIFT_ACCOUNT")
|
|
||||||
password = os.environ.get("SWIFT_PASSWORD")
|
|
||||||
project_name = os.environ.get("SWIFT_PROJECT_NAME")
|
|
||||||
if not project_name and os.environ.get("SWIFT_TENANT_NAME"):
|
|
||||||
project_name = os.environ["SWIFT_TENANT_NAME"]
|
|
||||||
_logger.warning(
|
|
||||||
"SWIFT_TENANT_NAME is deprecated and "
|
|
||||||
"must be replaced by SWIFT_PROJECT_NAME"
|
|
||||||
)
|
|
||||||
region = os.environ.get("SWIFT_REGION_NAME")
|
|
||||||
os_options = {}
|
|
||||||
if region:
|
|
||||||
os_options["region_name"] = region
|
|
||||||
if not (host and account and password and project_name):
|
|
||||||
raise exceptions.UserError(
|
|
||||||
_(
|
|
||||||
"Problem connecting to Swift store, are the env variables "
|
|
||||||
"(SWIFT_AUTH_URL, SWIFT_ACCOUNT, SWIFT_PASSWORD, "
|
|
||||||
"SWIFT_TENANT_NAME) properly set?"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
session = swift_session_store.get_session(
|
|
||||||
username=account,
|
|
||||||
password=password,
|
|
||||||
project_name=project_name,
|
|
||||||
auth_url=host,
|
|
||||||
)
|
|
||||||
conn = swiftclient.client.Connection(
|
|
||||||
session=session,
|
|
||||||
os_options=os_options,
|
|
||||||
)
|
|
||||||
except ClientException:
|
|
||||||
_logger.exception("Error connecting to Swift object store")
|
|
||||||
raise exceptions.UserError(_("Error on Swift connection")) from None
|
|
||||||
return conn
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _store_file_read(self, fname):
|
|
||||||
if fname.startswith("swift://"):
|
|
||||||
swifturi = SwiftUri(fname)
|
|
||||||
try:
|
|
||||||
conn = self._get_swift_connection()
|
|
||||||
except exceptions.UserError:
|
|
||||||
_logger.exception(
|
|
||||||
"error reading attachment '%s' from object storage", fname
|
|
||||||
)
|
|
||||||
return ""
|
|
||||||
try:
|
|
||||||
resp, read = conn.get_object(swifturi.container(), swifturi.item())
|
|
||||||
except ClientException:
|
|
||||||
read = ""
|
|
||||||
_logger.exception("Error reading object from Swift object store")
|
|
||||||
return read
|
|
||||||
else:
|
|
||||||
return super()._store_file_read(fname)
|
|
||||||
|
|
||||||
def _store_file_write(self, key, bin_data):
|
|
||||||
if self._storage() == "swift":
|
|
||||||
container = os.environ.get("SWIFT_WRITE_CONTAINER")
|
|
||||||
conn = self._get_swift_connection()
|
|
||||||
conn.put_container(container)
|
|
||||||
filename = "swift://{}/{}".format(container, key)
|
|
||||||
try:
|
|
||||||
conn.put_object(container, key, bin_data)
|
|
||||||
except ClientException:
|
|
||||||
_logger.exception("Error writing to Swift object store")
|
|
||||||
raise exceptions.UserError(_("Error writing to Swift")) from None
|
|
||||||
else:
|
|
||||||
_super = super()
|
|
||||||
filename = _super._store_file_write(key, bin_data)
|
|
||||||
return filename
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _store_file_delete(self, fname):
|
|
||||||
if fname.startswith("swift://"):
|
|
||||||
swifturi = SwiftUri(fname)
|
|
||||||
container = swifturi.container()
|
|
||||||
# delete the file only if it is on the current configured bucket
|
|
||||||
# otherwise, we might delete files used on a different environment
|
|
||||||
if container == os.environ.get("SWIFT_WRITE_CONTAINER"):
|
|
||||||
conn = self._get_swift_connection()
|
|
||||||
try:
|
|
||||||
conn.delete_object(container, swifturi.item())
|
|
||||||
except ClientException:
|
|
||||||
_logger.exception(_("Error deleting an object on the Swift store"))
|
|
||||||
# we ignore the error, file will stay on the object
|
|
||||||
# storage but won't disrupt the process
|
|
||||||
else:
|
|
||||||
return super()._file_delete_from_store(fname)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Copyright 2017-2019 Camptocamp SA
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class SwiftUri(object):
|
|
||||||
|
|
||||||
_url_re = re.compile("^swift:///*([^/]*)/?(.*)", re.IGNORECASE | re.UNICODE)
|
|
||||||
|
|
||||||
def __init__(self, uri):
|
|
||||||
match = self._url_re.match(uri)
|
|
||||||
if not match:
|
|
||||||
raise ValueError("%s: is not a valid Swift URI" % (uri,))
|
|
||||||
self._container, self._item = match.groups()
|
|
||||||
|
|
||||||
def container(self):
|
|
||||||
return self._container
|
|
||||||
|
|
||||||
def item(self):
|
|
||||||
return self._item
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import test_mock_swift_api
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
# Copyright 2017-2019 Camptocamp SA
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import os
|
|
||||||
|
|
||||||
import keystoneauth1
|
|
||||||
import mock
|
|
||||||
from mock import patch
|
|
||||||
|
|
||||||
from odoo.addons.attachment_swift.models.ir_attachment import SwiftSessionStore
|
|
||||||
from odoo.addons.attachment_swift.swift_uri import SwiftUri
|
|
||||||
from odoo.addons.base.tests.test_ir_attachment import TestIrAttachment
|
|
||||||
|
|
||||||
|
|
||||||
class TestAttachmentSwift(TestIrAttachment):
|
|
||||||
def setup(self):
|
|
||||||
res = super().setUp()
|
|
||||||
self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift")
|
|
||||||
return res
|
|
||||||
|
|
||||||
def test_session_store_get_session(self):
|
|
||||||
auth_url = "auth_url"
|
|
||||||
username = "username"
|
|
||||||
password = "password"
|
|
||||||
project_name = "project_name"
|
|
||||||
store = SwiftSessionStore()
|
|
||||||
session = store.get_session(
|
|
||||||
auth_url=auth_url,
|
|
||||||
username=username,
|
|
||||||
password=password,
|
|
||||||
project_name=project_name,
|
|
||||||
)
|
|
||||||
self.assertEqual(session.auth.auth_url, auth_url)
|
|
||||||
self.assertEqual(
|
|
||||||
session.auth.get_cache_id_elements().get("password_username"), username
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
session.auth.get_cache_id_elements().get("password_password"), password
|
|
||||||
)
|
|
||||||
self.assertEqual(session.auth.project_name, project_name)
|
|
||||||
|
|
||||||
# get the same session on a second call
|
|
||||||
self.assertEqual(
|
|
||||||
store.get_session(
|
|
||||||
auth_url=auth_url,
|
|
||||||
username=username,
|
|
||||||
password=password,
|
|
||||||
project_name=project_name,
|
|
||||||
),
|
|
||||||
session,
|
|
||||||
)
|
|
||||||
|
|
||||||
@patch("swiftclient.client")
|
|
||||||
def test_connection(self, mock_swift_client):
|
|
||||||
"""Test the connection to the store"""
|
|
||||||
os.environ["SWIFT_AUTH_URL"] = "auth_url"
|
|
||||||
os.environ["SWIFT_ACCOUNT"] = "account"
|
|
||||||
os.environ["SWIFT_PASSWORD"] = "password"
|
|
||||||
os.environ["SWIFT_PROJECT_NAME"] = "project_name"
|
|
||||||
os.environ["SWIFT_REGION_NAME"] = "NOWHERE"
|
|
||||||
attachment = self.Attachment
|
|
||||||
attachment._get_swift_connection()
|
|
||||||
mock_swift_client.Connection.assert_called_once_with(
|
|
||||||
session=mock.ANY,
|
|
||||||
os_options={"region_name": os.environ.get("SWIFT_REGION_NAME")},
|
|
||||||
)
|
|
||||||
__, kwargs = mock_swift_client.Connection.call_args
|
|
||||||
session = kwargs["session"]
|
|
||||||
self.assertTrue(isinstance(session, keystoneauth1.session.Session))
|
|
||||||
self.assertEqual(session.auth.auth_url, os.environ["SWIFT_AUTH_URL"])
|
|
||||||
self.assertEqual(
|
|
||||||
session.auth.get_cache_id_elements().get("password_username"),
|
|
||||||
os.environ["SWIFT_ACCOUNT"],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
session.auth.get_cache_id_elements().get("password_password"),
|
|
||||||
os.environ["SWIFT_PASSWORD"],
|
|
||||||
)
|
|
||||||
self.assertEqual(session.auth.project_name, os.environ["SWIFT_PROJECT_NAME"])
|
|
||||||
|
|
||||||
def test_store_file_on_swift(self):
|
|
||||||
"""
|
|
||||||
Test writing a file
|
|
||||||
"""
|
|
||||||
(self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift"))
|
|
||||||
os.environ["SWIFT_AUTH_URL"] = "auth_url"
|
|
||||||
os.environ["SWIFT_ACCOUNT"] = "account"
|
|
||||||
os.environ["SWIFT_PASSWORD"] = "password"
|
|
||||||
os.environ["SWIFT_PROJECT_NAME"] = "project_name"
|
|
||||||
os.environ["SWIFT_WRITE_CONTAINER"] = "my_container"
|
|
||||||
container = os.environ.get("SWIFT_WRITE_CONTAINER")
|
|
||||||
attachment = self.Attachment
|
|
||||||
bin_data = base64.b64decode(self.blob1_b64)
|
|
||||||
with patch("swiftclient.client.Connection") as MockConnection:
|
|
||||||
conn = MockConnection.return_value
|
|
||||||
attachment.create({"name": "a5", "datas": self.blob1_b64})
|
|
||||||
conn.put_object.assert_called_with(
|
|
||||||
container, attachment._compute_checksum(bin_data), bin_data
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_delete_file_on_swift(self):
|
|
||||||
"""
|
|
||||||
Test deleting a file
|
|
||||||
"""
|
|
||||||
(self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift"))
|
|
||||||
os.environ["SWIFT_AUTH_URL"] = "auth_url"
|
|
||||||
os.environ["SWIFT_ACCOUNT"] = "account"
|
|
||||||
os.environ["SWIFT_PASSWORD"] = "password"
|
|
||||||
os.environ["SWIFT_PROJECT_NAME"] = "project_name"
|
|
||||||
os.environ["SWIFT_WRITE_CONTAINER"] = "my_container"
|
|
||||||
|
|
||||||
attachment = self.Attachment
|
|
||||||
container = os.environ.get("SWIFT_WRITE_CONTAINER")
|
|
||||||
with patch("swiftclient.client.Connection") as MockConnection:
|
|
||||||
conn = MockConnection.return_value
|
|
||||||
a5 = attachment.create({"name": "a5", "datas": self.blob1_b64})
|
|
||||||
uri = SwiftUri(a5.store_fname)
|
|
||||||
a5.unlink()
|
|
||||||
conn.delete_object.assert_called_with(container, uri.item())
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Copyright 2017-2019 Camptocamp SA
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
|
||||||
|
|
||||||
from swiftclient.exceptions import ClientException
|
|
||||||
|
|
||||||
from odoo.addons.base.tests.test_ir_attachment import TestIrAttachment
|
|
||||||
|
|
||||||
from ..swift_uri import SwiftUri
|
|
||||||
|
|
||||||
|
|
||||||
class TestAttachmentSwift(TestIrAttachment):
|
|
||||||
"""
|
|
||||||
Those tests are made to be run against a real Swift store (local or remote)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
res = super().setUp()
|
|
||||||
self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift")
|
|
||||||
return res
|
|
||||||
|
|
||||||
def test_connection(self):
|
|
||||||
"""Test the connection to the Swift object store"""
|
|
||||||
conn = self.Attachment._get_swift_connection()
|
|
||||||
self.assertNotEqual(conn, False)
|
|
||||||
|
|
||||||
def test_store_file_on_swift(self):
|
|
||||||
"""Test writing a file and then reading it"""
|
|
||||||
(self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift"))
|
|
||||||
a5 = self.Attachment.create({"name": "a5", "datas": self.blob1_b64})
|
|
||||||
a5bis = self.Attachment.browse(a5.id)[0]
|
|
||||||
self.assertEqual(a5.datas, a5bis.datas)
|
|
||||||
|
|
||||||
def test_delete_file_on_swift(self):
|
|
||||||
"""Create a file and then test the deletion"""
|
|
||||||
(self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift"))
|
|
||||||
a5 = self.Attachment.create({"name": "a5", "datas": self.blob1_b64})
|
|
||||||
uri = SwiftUri(a5.store_fname)
|
|
||||||
con = self.Attachment._get_swift_connection()
|
|
||||||
con.get_object(uri.container(), uri.item())
|
|
||||||
a5.unlink()
|
|
||||||
with self.assertRaises(ClientException):
|
|
||||||
con.get_object(uri.container(), uri.item())
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Cloud Platform Exoscale
|
|
||||||
|
|
||||||
Install addons specific to the Exoscale setup.
|
|
||||||
|
|
||||||
* The object storage is S3
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import models
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Copyright 2017-2021 Camptocamp SA
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Cloud Platform Exoscale",
|
|
||||||
"summary": "Addons required for the Camptocamp Cloud Platform on Exoscale",
|
|
||||||
"version": "15.0.1.0.0",
|
|
||||||
"author": "Camptocamp,Odoo Community Association (OCA)",
|
|
||||||
"license": "AGPL-3",
|
|
||||||
"category": "Extra Tools",
|
|
||||||
"depends": [
|
|
||||||
"cloud_platform",
|
|
||||||
"attachment_s3",
|
|
||||||
"monitoring_statsd",
|
|
||||||
],
|
|
||||||
"excludes": [
|
|
||||||
"cloud_platform_ovh",
|
|
||||||
],
|
|
||||||
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
|
||||||
"data": [],
|
|
||||||
"installable": False,
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import cloud_platform
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
# Copyright 2016-2019 Camptocamp SA
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from odoo import api, models
|
|
||||||
|
|
||||||
from odoo.addons.cloud_platform.models.cloud_platform import (
|
|
||||||
FilestoreKind,
|
|
||||||
PlatformConfig,
|
|
||||||
)
|
|
||||||
|
|
||||||
S3_STORE_KIND = FilestoreKind("s3", "remote")
|
|
||||||
|
|
||||||
|
|
||||||
class CloudPlatform(models.AbstractModel):
|
|
||||||
_inherit = "cloud.platform"
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _filestore_kinds(self):
|
|
||||||
kinds = super(CloudPlatform, self)._filestore_kinds()
|
|
||||||
kinds["s3"] = S3_STORE_KIND
|
|
||||||
return kinds
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _platform_kinds(self):
|
|
||||||
kinds = super(CloudPlatform, self)._platform_kinds()
|
|
||||||
kinds.append("exoscale")
|
|
||||||
return kinds
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _config_by_server_env_for_exoscale(self):
|
|
||||||
fs_kinds = self._filestore_kinds()
|
|
||||||
configs = {
|
|
||||||
"prod": PlatformConfig(filestore=fs_kinds["s3"]),
|
|
||||||
"integration": PlatformConfig(filestore=fs_kinds["s3"]),
|
|
||||||
"labs": PlatformConfig(filestore=fs_kinds["s3"]),
|
|
||||||
"test": PlatformConfig(filestore=fs_kinds["db"]),
|
|
||||||
"dev": PlatformConfig(filestore=fs_kinds["db"]),
|
|
||||||
}
|
|
||||||
return configs
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _check_filestore(self, environment_name):
|
|
||||||
params = self.env["ir.config_parameter"].sudo()
|
|
||||||
use_s3 = params.get_param("ir_attachment.location") == S3_STORE_KIND.name
|
|
||||||
if environment_name in ("prod", "integration"):
|
|
||||||
# Labs instances use 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!
|
|
||||||
assert use_s3, (
|
|
||||||
"S3 must be used on production and integration instances. "
|
|
||||||
"It is activated by setting 'ir_attachment.location.' to 's3'."
|
|
||||||
" The 'install()' function sets this option "
|
|
||||||
"automatically."
|
|
||||||
)
|
|
||||||
if use_s3:
|
|
||||||
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"), (
|
|
||||||
"AWS_SECRET_ACCESS_KEY environment variable is required when "
|
|
||||||
"ir_attachment.location is 's3'."
|
|
||||||
)
|
|
||||||
bucket_name = os.environ.get("AWS_BUCKETNAME", "")
|
|
||||||
if environment_name in ("prod", "integration", "labs"):
|
|
||||||
assert bucket_name, (
|
|
||||||
"AWS_BUCKETNAME environment variable is required when "
|
|
||||||
"ir_attachment.location is 's3'.\n"
|
|
||||||
"Normally, 's3' 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
|
|
||||||
# <client>-odoo-<env>
|
|
||||||
#
|
|
||||||
# Use AWS_BUCKETNAME_UNSTRUCTURED to by-pass check on bucket name
|
|
||||||
# structure
|
|
||||||
if os.environ.get("AWS_BUCKETNAME_UNSTRUCTURED"):
|
|
||||||
return
|
|
||||||
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,)
|
|
||||||
)
|
|
||||||
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, (
|
|
||||||
"AWS_BUCKETNAME should not match '<client>-odoo-prod', "
|
|
||||||
"we got: '%s'" % (bucket_name,)
|
|
||||||
)
|
|
||||||
|
|
||||||
elif environment_name == "test":
|
|
||||||
# store in DB so we don't have files local to the host
|
|
||||||
assert params.get_param("ir_attachment.location") == "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()'."
|
|
||||||
)
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def install(self):
|
|
||||||
self._install("exoscale")
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Cloud Platform OVH
|
|
||||||
|
|
||||||
Install addons specific to the OVH setup.
|
|
||||||
|
|
||||||
* The object storage is Swift
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import models
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Copyright 2017-2021 Camptocamp SA
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Cloud Platform OVH",
|
|
||||||
"summary": "Addons required for the Camptocamp Cloud Platform on OVH",
|
|
||||||
"version": "15.0.1.0.0",
|
|
||||||
"author": "Camptocamp,Odoo Community Association (OCA)",
|
|
||||||
"license": "AGPL-3",
|
|
||||||
"category": "Extra Tools",
|
|
||||||
"depends": [
|
|
||||||
"cloud_platform",
|
|
||||||
"attachment_swift",
|
|
||||||
"monitoring_statsd",
|
|
||||||
],
|
|
||||||
"excludes": [
|
|
||||||
"cloud_platform_exoscale",
|
|
||||||
],
|
|
||||||
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
|
||||||
"data": [],
|
|
||||||
"installable": False,
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import cloud_platform
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
# Copyright 2017-2019 Camptocamp SA
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from odoo import api, models
|
|
||||||
|
|
||||||
from odoo.addons.cloud_platform.models.cloud_platform import (
|
|
||||||
FilestoreKind,
|
|
||||||
PlatformConfig,
|
|
||||||
)
|
|
||||||
|
|
||||||
SWIFT_STORE_KIND = FilestoreKind("swift", "remote")
|
|
||||||
|
|
||||||
|
|
||||||
class CloudPlatform(models.AbstractModel):
|
|
||||||
_inherit = "cloud.platform"
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _filestore_kinds(self):
|
|
||||||
kinds = super(CloudPlatform, self)._filestore_kinds()
|
|
||||||
kinds["swift"] = SWIFT_STORE_KIND
|
|
||||||
return kinds
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _platform_kinds(self):
|
|
||||||
kinds = super()._platform_kinds()
|
|
||||||
kinds.append("ovh")
|
|
||||||
return kinds
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _config_by_server_env_for_ovh(self):
|
|
||||||
fs_kinds = self._filestore_kinds()
|
|
||||||
configs = {
|
|
||||||
"prod": PlatformConfig(filestore=fs_kinds["swift"]),
|
|
||||||
"integration": PlatformConfig(filestore=fs_kinds["swift"]),
|
|
||||||
"labs": PlatformConfig(filestore=fs_kinds["swift"]),
|
|
||||||
"test": PlatformConfig(filestore=fs_kinds["db"]),
|
|
||||||
"dev": PlatformConfig(filestore=fs_kinds["db"]),
|
|
||||||
}
|
|
||||||
return configs
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _check_filestore(self, environment_name):
|
|
||||||
params = self.env["ir.config_parameter"].sudo()
|
|
||||||
use_swift = params.get_param("ir_attachment.location") == SWIFT_STORE_KIND.name
|
|
||||||
if environment_name in ("prod", "integration"):
|
|
||||||
# Labs instances use swift 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_swift, (
|
|
||||||
"Swift must be used on production and integration instances. "
|
|
||||||
"It is activated, setting 'ir_attachment.location.' to 'swift'"
|
|
||||||
" The 'install()' function sets this option "
|
|
||||||
"automatically."
|
|
||||||
)
|
|
||||||
if use_swift:
|
|
||||||
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"), (
|
|
||||||
"SWIFT_ACCOUNT environment variable is required when "
|
|
||||||
"ir_attachment.location is 'swift'."
|
|
||||||
)
|
|
||||||
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", "")
|
|
||||||
if environment_name in ("prod", "integration", "labs"):
|
|
||||||
assert container_name, (
|
|
||||||
"SWIFT_WRITE_CONTAINER environment variable is required when "
|
|
||||||
"ir_attachment.location is 'swift'.\n"
|
|
||||||
"Normally, 'swift' 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."
|
|
||||||
)
|
|
||||||
prod_container = bool(re.match(r"[a-z0-9-]+-odoo-prod", container_name))
|
|
||||||
# A bucket name is defined under the following format
|
|
||||||
# <client>-odoo-<env>
|
|
||||||
#
|
|
||||||
# Use SWIFT_WRITE_CONTAINER_UNSTRUCTURED to by-pass check on bucket name
|
|
||||||
# structure
|
|
||||||
if os.environ.get("SWIFT_WRITE_CONTAINER_UNSTRUCTURED"):
|
|
||||||
return
|
|
||||||
if environment_name == "prod":
|
|
||||||
assert prod_container, (
|
|
||||||
"SWIFT_WRITE_CONTAINER should match '<client>-odoo-prod', "
|
|
||||||
"we got: '%s'" % (container_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_container, (
|
|
||||||
"SWIFT_WRITE_CONTAINER should not match "
|
|
||||||
"'<client>-odoo-prod', we got: '%s'" % (container_name,)
|
|
||||||
)
|
|
||||||
elif environment_name == "test":
|
|
||||||
# store in DB so we don't have files local to the host
|
|
||||||
assert params.get_param("ir_attachment.location") == "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()'."
|
|
||||||
)
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def install(self):
|
|
||||||
self._install("ovh")
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
|
|
||||||
:alt: License
|
|
||||||
|
|
||||||
============================
|
|
||||||
Kwktnkltopdf Asset
|
|
||||||
============================
|
|
||||||
|
|
||||||
Force assets to be saved to be readable by kwkhtmltopdf server, the
|
|
||||||
first time you print a report it failed to generate
|
|
||||||
the pdf asset used for pdf
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import models
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Copyright 2016-2021 Camptocamp SA
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": "Kwkhtmltopdf: Asset fix",
|
|
||||||
"version": "17.0.1.0.0",
|
|
||||||
"author": "Camptocamp,Odoo Community Association (OCA)",
|
|
||||||
"license": "AGPL-3",
|
|
||||||
"category": "category",
|
|
||||||
"depends": [
|
|
||||||
"base",
|
|
||||||
],
|
|
||||||
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
|
||||||
"data": [],
|
|
||||||
"installable": True,
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import ir_qweb
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# Copyright 2016-2019 Camptocamp SA
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
|
||||||
|
|
||||||
from odoo import models
|
|
||||||
from odoo.tools import config
|
|
||||||
|
|
||||||
|
|
||||||
class IrQweb(models.AbstractModel):
|
|
||||||
|
|
||||||
_inherit = "ir.qweb"
|
|
||||||
|
|
||||||
def _generate_asset_nodes_cache(
|
|
||||||
self,
|
|
||||||
bundle,
|
|
||||||
css=True,
|
|
||||||
js=True,
|
|
||||||
debug=False,
|
|
||||||
async_load=False,
|
|
||||||
defer_load=False,
|
|
||||||
lazy_load=False,
|
|
||||||
media=None,
|
|
||||||
):
|
|
||||||
context_for_printing = self.env.context.copy()
|
|
||||||
if not config["test_enable"]:
|
|
||||||
context_for_printing["commit_assetsbundle"] = True
|
|
||||||
return super(
|
|
||||||
IrQweb, self.with_context(**context_for_printing)
|
|
||||||
)._generate_asset_nodes(
|
|
||||||
bundle, css, js, debug, async_load, defer_load, lazy_load, media
|
|
||||||
)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../../../kwkhtmltopdf_assets
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import setuptools
|
|
||||||
|
|
||||||
setuptools.setup(
|
|
||||||
setup_requires=['setuptools-odoo'],
|
|
||||||
odoo_addon=True,
|
|
||||||
)
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
{
|
{
|
||||||
"name": "test base fileurl fields",
|
"name": "test base fileurl fields",
|
||||||
"summary": """A module to verify fileurl field.""",
|
"summary": """A module to verify fileurl field.""",
|
||||||
"version": "12.0.1.0.0",
|
"version": "17.0.1.0.0",
|
||||||
"category": "Tests",
|
"category": "Tests",
|
||||||
"author": "Camptocamp,Odoo Community Association (OCA)",
|
"author": "Camptocamp,Odoo Community Association (OCA)",
|
||||||
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||||
|
|||||||
Reference in New Issue
Block a user