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:
committed 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",
|
||||
"summary": """A module to verify fileurl field.""",
|
||||
"version": "12.0.1.0.0",
|
||||
"version": "17.0.1.0.0",
|
||||
"category": "Tests",
|
||||
"author": "Camptocamp,Odoo Community Association (OCA)",
|
||||
"website": "https://github.com/camptocamp/odoo-cloud-platform",
|
||||
|
||||
Reference in New Issue
Block a user