mirror of
https://github.com/camptocamp/odoo-cloud-platform.git
synced 2026-06-24 02:08:36 +00:00
Merge pull request #16 from TDu/add-ovh-platform
[BSRTL-215] Add OVH as an option for the cloud platform
This commit is contained in:
committed by
GitHub
co-authored by
GitHub
commit
86e5bd3ad7
@@ -12,7 +12,15 @@ On the platform we want to achieve having:
|
|||||||
* Logs sent to ElasticSearch-Kibana structured as JSON for better searching
|
* Logs sent to ElasticSearch-Kibana structured as JSON for better searching
|
||||||
|
|
||||||
For the storage, we store all the attachments on a object storage such as S3 or
|
For the storage, we store all the attachments on a object storage such as S3 or
|
||||||
a S3 compatible one, and we store the werkzeug sessions on Redis.
|
Swift, and we store the werkzeug sessions on Redis.
|
||||||
|
|
||||||
|
Two providers are available for the Cloud Platform, Exoscale based in
|
||||||
|
Switzerland and OVH in France.
|
||||||
|
|
||||||
|
The main difference between the two is the Object Store they use :
|
||||||
|
|
||||||
|
* Exoscale uses S3
|
||||||
|
* OVH uses Swift
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
@@ -21,23 +29,30 @@ a S3 compatible one, and we store the werkzeug sessions on Redis.
|
|||||||
Libraries that must be added in ``requirements.txt``:
|
Libraries that must be added in ``requirements.txt``:
|
||||||
|
|
||||||
```
|
```
|
||||||
boto==2.42.0
|
|
||||||
redis==2.10.5
|
redis==2.10.5
|
||||||
python-json-logger==0.1.5
|
python-json-logger==0.1.5
|
||||||
statsd==3.2.1
|
statsd==3.2.1
|
||||||
|
|
||||||
|
# For S3 object storage (Exoscale, AWS)
|
||||||
|
boto==2.42.0
|
||||||
|
|
||||||
|
# For Swift object storage (OVH)
|
||||||
|
python-swiftclient==3.4.0
|
||||||
|
python-keystoneclient==3.13.0
|
||||||
```
|
```
|
||||||
|
|
||||||
### Odoo Startup
|
### Odoo Startup
|
||||||
|
|
||||||
The `--load` option of Odoo must contains the following addons:
|
The `--load` option of Odoo must contains the following addons:
|
||||||
|
|
||||||
* `attachment_s3`
|
* `attachment_s3` or `attachment_swift` depending of the provider used.
|
||||||
* `session_redis`
|
* `session_redis`
|
||||||
* `logging_json`
|
* `logging_json`
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
`--load=web,web_kanban,attachment_s3,session_redis,logging_json`
|
`--load=web,web_kanban,attachment_s3,session_redis,logging_json`
|
||||||
|
`--load=web,web_kanban,attachment_swift,session_redis,logging_json`
|
||||||
|
|
||||||
### Server Environment
|
### Server Environment
|
||||||
|
|
||||||
@@ -51,7 +66,7 @@ The server environments in `server_environment_files` must be at least:
|
|||||||
The exact naming is important, because the `cloud_platform` addon rely on these keys to know and check the running environment.
|
The exact naming is important, because the `cloud_platform` addon rely on these keys to know and check the running environment.
|
||||||
|
|
||||||
|
|
||||||
### Attachments in the Object Storage
|
### Attachments in the Object Storage S3
|
||||||
|
|
||||||
* prod: stored RW in the object storage
|
* prod: stored RW in the object storage
|
||||||
* `AWS_HOST`: depends of the platform
|
* `AWS_HOST`: depends of the platform
|
||||||
@@ -69,6 +84,25 @@ Besides, the attachment location should be set to `s3` (but this is
|
|||||||
automatically done by the `install` methods of the `cloud_platform` module.
|
automatically done by the `install` methods of the `cloud_platform` module.
|
||||||
* `ir.config_parameter` `ir_attachment.location`: `s3`
|
* `ir.config_parameter` `ir_attachment.location`: `s3`
|
||||||
|
|
||||||
|
|
||||||
|
### Attachments in the Object Storage Swift
|
||||||
|
|
||||||
|
* prod: stored RW in the object storage
|
||||||
|
* `SWIFT_AUTH_URL`: depends of the platform
|
||||||
|
* `SWIFT_ACCOUNT`: depends of the platform
|
||||||
|
* `SWIFT_PASSWORD`: depends of the platform
|
||||||
|
* `SWIFT_WRITE_CONTAINER`: `<client>-odoo-prod`
|
||||||
|
* integration:
|
||||||
|
* `SWIFT_AUTH_URL`: depends of the platform
|
||||||
|
* `SWIFT_ACCOUNT`: depends of the platform
|
||||||
|
* `SWIFT_PASSWORD`: depends of the platform
|
||||||
|
* `SWIFT_WRITE_CONTAINER`: `<client>-odoo-integration`
|
||||||
|
* test: attachments are stored in database
|
||||||
|
|
||||||
|
Besides, the attachment location should be set to `swift` (but this is
|
||||||
|
automatically done by the `install` methods of the `cloud_platform` module.
|
||||||
|
* `ir.config_parameter` `ir_attachment.location`: `swift`
|
||||||
|
|
||||||
### Sessions in Redis
|
### Sessions in Redis
|
||||||
|
|
||||||
* prod:
|
* prod:
|
||||||
@@ -108,11 +142,18 @@ Should be active at least on the production server
|
|||||||
|
|
||||||
### Automatic Configuration
|
### Automatic Configuration
|
||||||
|
|
||||||
Calling `ctx.env['cloud.platform'].install_exoscale()` in an
|
An automatic configuration can be executed from an `anthem` song to configure
|
||||||
`anthem` song will configure some parameters such as the
|
some parameters such as the `ir_attachment.location` and migrate the existing
|
||||||
`ir_attachment.location` and migrate the existing attachments to the
|
attachments to the object storage.
|
||||||
object storage.
|
|
||||||
|
|
||||||
|
It can be called like this:
|
||||||
|
`ctx.env['cloud.platform'].install(cloud_platform_kind)`
|
||||||
|
Replacing `cloud_platform_kind` with 'exoscale' or 'ovh'
|
||||||
|
|
||||||
|
Or using one of the direct shortcuts:
|
||||||
|
|
||||||
|
* `ctx.env['cloud.platform'].install_exoscale()`
|
||||||
|
* `ctx.env['cloud.platform'].install_ovh()`
|
||||||
|
|
||||||
### Startup checks
|
### Startup checks
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
'author': 'Camptocamp,Odoo Community Association (OCA)',
|
'author': 'Camptocamp,Odoo Community Association (OCA)',
|
||||||
'license': 'AGPL-3',
|
'license': 'AGPL-3',
|
||||||
'category': 'Knowledge Management',
|
'category': 'Knowledge Management',
|
||||||
'depends': ['base'],
|
'depends': ['base', 'base_attachment_object_storage'],
|
||||||
'external_dependencies': {
|
'external_dependencies': {
|
||||||
'python': ['boto'],
|
'python': ['boto'],
|
||||||
},
|
},
|
||||||
'website': 'http://www.camptocamp.com',
|
'website': 'https://www.camptocamp.com',
|
||||||
'data': [],
|
'data': [],
|
||||||
'installable': True,
|
'installable': True,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,8 @@ import base64
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import xml.dom.minidom
|
import xml.dom.minidom
|
||||||
from contextlib import closing, contextmanager
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
import psycopg2
|
|
||||||
|
|
||||||
import odoo
|
|
||||||
from odoo import _, api, exceptions, models
|
from odoo import _, api, exceptions, models
|
||||||
from ..s3uri import S3Uri
|
from ..s3uri import S3Uri
|
||||||
|
|
||||||
@@ -30,67 +26,10 @@ except ImportError:
|
|||||||
class IrAttachment(models.Model):
|
class IrAttachment(models.Model):
|
||||||
_inherit = "ir.attachment"
|
_inherit = "ir.attachment"
|
||||||
|
|
||||||
@api.multi
|
def _get_stores(self):
|
||||||
def _store_in_db_when_s3(self):
|
l = ['s3']
|
||||||
""" Return whether an attachment must be stored in db
|
l += super(IrAttachment, self)._get_stores()
|
||||||
|
return l
|
||||||
When we are using S3. This is sometimes required because
|
|
||||||
the object storage is slower than the database/filesystem.
|
|
||||||
|
|
||||||
We store image_small and image_medium from 'Binary' fields
|
|
||||||
because they should be fast to read as they are often displayed
|
|
||||||
in kanbans / lists. The same for web_icon_data.
|
|
||||||
|
|
||||||
We store the assets locally as well. Not only for performance,
|
|
||||||
but also because it improves the portability of the database:
|
|
||||||
when assets are invalidated, they are deleted so we don't have
|
|
||||||
an old database with attachments pointing to deleted assets.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.ensure_one()
|
|
||||||
|
|
||||||
# assets
|
|
||||||
if self.res_model == 'ir.ui.view':
|
|
||||||
# assets are stored in 'ir.ui.view'
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Binary fields
|
|
||||||
if self.res_field:
|
|
||||||
# Binary fields are stored with the name of the field in
|
|
||||||
# 'res_field'
|
|
||||||
local_fields = ('image_small', 'image_medium', 'web_icon_data')
|
|
||||||
# 'image' fields can be rather large and should usually
|
|
||||||
# not be requests in bulk in lists
|
|
||||||
if self.res_field and self.res_field in local_fields:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _inverse_datas(self):
|
|
||||||
# override in order to store files that need fast access,
|
|
||||||
# we keep them in the database instead of the object storage
|
|
||||||
location = self._storage()
|
|
||||||
for attach in self:
|
|
||||||
if location == 's3' and self._store_in_db_when_s3():
|
|
||||||
# compute the fields that depend on datas
|
|
||||||
value = attach.datas
|
|
||||||
bin_data = value and value.decode('base64') or ''
|
|
||||||
vals = {
|
|
||||||
'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,
|
|
||||||
}
|
|
||||||
fname = attach.store_fname
|
|
||||||
# write as superuser, as user probably does not
|
|
||||||
# have write access
|
|
||||||
super(IrAttachment, attach.sudo()).write(vals)
|
|
||||||
if fname:
|
|
||||||
self._file_delete(fname)
|
|
||||||
continue
|
|
||||||
super(IrAttachment, attach)._inverse_datas()
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_s3_bucket(self, name=None):
|
def _get_s3_bucket(self, name=None):
|
||||||
@@ -157,7 +96,8 @@ class IrAttachment(models.Model):
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _file_read_s3(self, fname, bin_size=False):
|
def _store_file_read(self, fname, bin_size=False):
|
||||||
|
if fname.startswith('s3://'):
|
||||||
s3uri = S3Uri(fname)
|
s3uri = S3Uri(fname)
|
||||||
try:
|
try:
|
||||||
bucket = self._get_s3_bucket(name=s3uri.bucket())
|
bucket = self._get_s3_bucket(name=s3uri.bucket())
|
||||||
@@ -171,24 +111,17 @@ class IrAttachment(models.Model):
|
|||||||
read = base64.b64encode(filekey.get_contents_as_string())
|
read = base64.b64encode(filekey.get_contents_as_string())
|
||||||
else:
|
else:
|
||||||
read = ''
|
read = ''
|
||||||
_logger.info("attachment '%s' missing on object storage", fname)
|
_logger.info(
|
||||||
|
"attachment '%s' missing on object storage", fname
|
||||||
|
)
|
||||||
return read
|
return read
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _file_read(self, fname, bin_size=False):
|
|
||||||
if fname.startswith('s3://'):
|
|
||||||
return self._file_read_s3(fname, bin_size=bin_size)
|
|
||||||
else:
|
else:
|
||||||
_super = super(IrAttachment, self)
|
return super(IrAttachment, self)._store_file_read(fname, bin_size)
|
||||||
return _super._file_read(fname, bin_size=bin_size)
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _file_write(self, value, checksum):
|
def _store_file_write(self, key, bin_data):
|
||||||
storage = self._storage()
|
if self._storage() == 's3':
|
||||||
if storage == 's3':
|
|
||||||
bucket = self._get_s3_bucket()
|
bucket = self._get_s3_bucket()
|
||||||
bin_data = value.decode('base64')
|
|
||||||
key = self._compute_checksum(bin_data)
|
|
||||||
filekey = bucket.get_key(key) or bucket.new_key(key)
|
filekey = bucket.get_key(key) or bucket.new_key(key)
|
||||||
filename = 's3://%s/%s' % (bucket.name, key)
|
filename = 's3://%s/%s' % (bucket.name, key)
|
||||||
try:
|
try:
|
||||||
@@ -203,18 +136,13 @@ class IrAttachment(models.Model):
|
|||||||
(self._parse_s3_error(error),)
|
(self._parse_s3_error(error),)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
filename = super(IrAttachment, self)._file_write(value, checksum)
|
_super = super(IrAttachment, self)
|
||||||
|
filename = _super._store_file_write(key, bin_data)
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _file_delete(self, fname):
|
def _store_file_delete(self, fname):
|
||||||
if fname.startswith('s3://'):
|
if fname.startswith('s3://'):
|
||||||
# using SQL to include files hidden through unlink or due to record
|
|
||||||
# rules
|
|
||||||
cr = self.env.cr
|
|
||||||
cr.execute("SELECT COUNT(*) FROM ir_attachment "
|
|
||||||
"WHERE store_fname = %s", (fname,))
|
|
||||||
count = cr.fetchone()[0]
|
|
||||||
s3uri = S3Uri(fname)
|
s3uri = S3Uri(fname)
|
||||||
bucket_name = s3uri.bucket()
|
bucket_name = s3uri.bucket()
|
||||||
item_name = s3uri.item()
|
item_name = s3uri.item()
|
||||||
@@ -223,7 +151,7 @@ class IrAttachment(models.Model):
|
|||||||
if bucket_name == os.environ.get('AWS_BUCKETNAME'):
|
if bucket_name == os.environ.get('AWS_BUCKETNAME'):
|
||||||
bucket = self._get_s3_bucket()
|
bucket = self._get_s3_bucket()
|
||||||
filekey = bucket.get_key(item_name)
|
filekey = bucket.get_key(item_name)
|
||||||
if not count and filekey:
|
if filekey:
|
||||||
try:
|
try:
|
||||||
filekey.delete()
|
filekey.delete()
|
||||||
_logger.info(
|
_logger.info(
|
||||||
@@ -236,121 +164,4 @@ class IrAttachment(models.Model):
|
|||||||
'Error during deletion of the file %s' % fname
|
'Error during deletion of the file %s' % fname
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
super(IrAttachment, self)._file_delete(fname)
|
super(IrAttachment, self)._store_file_delete(fname)
|
||||||
|
|
||||||
@api.multi
|
|
||||||
def _move_attachment_to_s3(self):
|
|
||||||
self.ensure_one()
|
|
||||||
_logger.info('inspecting attachment %s (%d)',
|
|
||||||
self.name, self.id)
|
|
||||||
fname = self.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({'datas': self.datas,
|
|
||||||
# this is required otherwise the
|
|
||||||
# mimetype gets overriden with
|
|
||||||
# 'application/octet-stream'
|
|
||||||
# on assets
|
|
||||||
'mimetype': self.mimetype})
|
|
||||||
_logger.info('moved %s on the object storage', fname)
|
|
||||||
full_path = self._full_path(fname)
|
|
||||||
_logger.info('cleaning fs self')
|
|
||||||
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
|
|
||||||
)
|
|
||||||
except IOError:
|
|
||||||
# Harmless and needed for race conditions
|
|
||||||
_logger.info(
|
|
||||||
"_file_delete could not unlink %s",
|
|
||||||
full_path, exc_info=True
|
|
||||||
)
|
|
||||||
elif self.db_datas:
|
|
||||||
_logger.info('moving on the object storage from database')
|
|
||||||
self.write({'datas': self.datas})
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _force_storage_s3(self, new_cr=False):
|
|
||||||
if not self.env['res.users'].browse(self.env.uid)._is_admin():
|
|
||||||
raise exceptions.AccessError(
|
|
||||||
_('Only administrators can execute this action.')
|
|
||||||
)
|
|
||||||
|
|
||||||
storage = self._storage()
|
|
||||||
if storage != 's3':
|
|
||||||
return
|
|
||||||
_logger.info('migrating files to the object storage')
|
|
||||||
domain = ['!', ('store_fname', '=like', 's3://%'),
|
|
||||||
'|',
|
|
||||||
('res_field', '=', False),
|
|
||||||
('res_field', '!=', False)]
|
|
||||||
# We do a copy of the environment so we can workaround the
|
|
||||||
# cache issue below. We do not create a new cursor because
|
|
||||||
# it causes serialization issues due to concurrent updates on
|
|
||||||
# attachments during the installation
|
|
||||||
with self.do_in_new_env(new_cr=new_cr) as new_env:
|
|
||||||
attachment_model_env = new_env['ir.attachment']
|
|
||||||
ids = attachment_model_env.search(domain).ids
|
|
||||||
for attachment_id in ids:
|
|
||||||
try:
|
|
||||||
with new_env.cr.savepoint():
|
|
||||||
# check that no other transaction has
|
|
||||||
# locked the row, don't send a file to S3
|
|
||||||
# in that case
|
|
||||||
self.env.cr.execute("SELECT id "
|
|
||||||
"FROM ir_attachment "
|
|
||||||
"WHERE id = %s "
|
|
||||||
"FOR UPDATE NOWAIT",
|
|
||||||
(attachment_id,),
|
|
||||||
log_exceptions=False)
|
|
||||||
|
|
||||||
# This is a trick to avoid having the 'datas' function
|
|
||||||
# fields computed for every attachment on each
|
|
||||||
# iteration of the loop. The former issue being that
|
|
||||||
# it reads the content of the file of ALL the
|
|
||||||
# attachments on each loop.
|
|
||||||
new_env.clear()
|
|
||||||
attachment = attachment_model_env.browse(attachment_id)
|
|
||||||
attachment._move_attachment_to_s3()
|
|
||||||
except psycopg2.OperationalError:
|
|
||||||
_logger.error('Could not migrate attachment %s to S3',
|
|
||||||
attachment_id)
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def do_in_new_env(self, new_cr=False):
|
|
||||||
""" Context manager that yields a new environment
|
|
||||||
|
|
||||||
Using a new Odoo Environment thus a new PG transaction.
|
|
||||||
"""
|
|
||||||
with api.Environment.manage():
|
|
||||||
if new_cr:
|
|
||||||
registry = odoo.modules.registry.RegistryManager.get(
|
|
||||||
self.env.cr.dbname
|
|
||||||
)
|
|
||||||
with closing(registry.cursor()) as cr:
|
|
||||||
try:
|
|
||||||
yield self.env(cr=cr)
|
|
||||||
except:
|
|
||||||
cr.rollback()
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
# disable pylint error because this is a valid commit,
|
|
||||||
# we are in a new env
|
|
||||||
cr.commit() # pylint: disable=invalid-commit
|
|
||||||
else:
|
|
||||||
# make a copy
|
|
||||||
yield self.env()
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def force_storage(self):
|
|
||||||
storage = self._storage()
|
|
||||||
if storage == 's3':
|
|
||||||
self._force_storage_s3()
|
|
||||||
else:
|
|
||||||
return super(IrAttachment, self).force_storage()
|
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
Attachments on Swift storage
|
Attachments on Swift storage
|
||||||
============================
|
============================
|
||||||
|
|
||||||
This addon allows to store the attachments (documents and assets) on
|
This addon enable storing attachments (documents and assets) on OpenStack Object Storage (Swift)
|
||||||
OpenStack Object Storage (Swift)
|
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
Activate Swift storage:
|
Activate Swift storage:
|
||||||
|
|
||||||
* Create or set the system parameter with the key ``ir_attachment.location``
|
* Create or set the system parameter with the key ``ir_attachment.location`` with the following value ``swift``.
|
||||||
and the value in the form ``swift``.
|
|
||||||
|
|
||||||
Configure accesses with environment variables:
|
Configure accesses with environment variables:
|
||||||
|
|
||||||
* ``SWIFT_HOST``
|
* ``SWIFT_AUTH_URL`` : URL of the Swift server
|
||||||
|
* ``SWIFT_TENANT_NAME``
|
||||||
* ``SWIFT_ACCOUNT``
|
* ``SWIFT_ACCOUNT``
|
||||||
* ``SWIFT_PASSWORD``
|
* ``SWIFT_PASSWORD``
|
||||||
* ``SWIFT_WRITE_CONTAINER``
|
* ``SWIFT_WRITE_CONTAINER`` : Name of the container to use in the store (created if not existing)
|
||||||
|
|
||||||
Read-only mode:
|
Read-only mode:
|
||||||
|
|
||||||
@@ -32,3 +31,21 @@ credentials) without any risk to alter the production data.
|
|||||||
This addon must be added in the server wide addons with (``--load`` option):
|
This addon must be added in the server wide addons with (``--load`` option):
|
||||||
|
|
||||||
``--load=web,web_kanban,attachment_swift``
|
``--load=web,web_kanban,attachment_swift``
|
||||||
|
|
||||||
|
Python Dependencies
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
This module needs the python-swiftclient and the python-keystoneclient (For auth v2.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:
|
||||||
|
|
||||||
|
export AUTH_VERSION=2.0
|
||||||
|
export OS_USERNAME={SWIFT_ACCOUNT}
|
||||||
|
export OS_PASSWORD={SWIFT_PASSWORD}
|
||||||
|
export OS_TENANT_NAME={SWIFT_TENANT_NAME}
|
||||||
|
export OS_AUTH_URL=https://auth.cloud.ovh.net/v2.0
|
||||||
|
swift stat
|
||||||
|
|
||||||
|
More information at
|
||||||
|
https://docs.openstack.org/python-swiftclient/latest/cli/index.html#swift-usage
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2016 Camptocamp SA
|
# Copyright 2017 Camptocamp SA
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||||
|
|
||||||
|
|
||||||
@@ -11,9 +11,11 @@
|
|||||||
'category': 'Knowledge Management',
|
'category': 'Knowledge Management',
|
||||||
'depends': ['base_attachment_object_storage'],
|
'depends': ['base_attachment_object_storage'],
|
||||||
'external_dependencies': {
|
'external_dependencies': {
|
||||||
'python': ['swiftclient'],
|
'python': ['swiftclient',
|
||||||
|
'keystoneclient',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
'website': 'http://www.camptocamp.com',
|
'website': 'https://www.camptocamp.com',
|
||||||
'data': [],
|
'data': [],
|
||||||
'installable': True,
|
'installable': True,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,23 +24,30 @@ except ImportError:
|
|||||||
class IrAttachment(models.Model):
|
class IrAttachment(models.Model):
|
||||||
_inherit = 'ir.attachment'
|
_inherit = 'ir.attachment'
|
||||||
|
|
||||||
_SWIFT_STORAGE = 'swift'
|
def _get_stores(self):
|
||||||
|
l = ['swift']
|
||||||
|
l += super(IrAttachment, self)._get_stores()
|
||||||
|
return l
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_swift_connection(self):
|
def _get_swift_connection(self):
|
||||||
""" Returns a connection object for the Swift object store """
|
""" Returns a connection object for the Swift object store """
|
||||||
host = os.environ.get('SWIFT_HOST')
|
host = os.environ.get('SWIFT_AUTH_URL')
|
||||||
account = os.environ.get('SWIFT_ACCOUNT')
|
account = os.environ.get('SWIFT_ACCOUNT')
|
||||||
password = os.environ.get('SWIFT_PASSWORD')
|
password = os.environ.get('SWIFT_PASSWORD')
|
||||||
if not (host and account and password):
|
tenant_name = os.environ.get('SWIFT_TENANT_NAME')
|
||||||
|
if not (host and account and password and tenant_name):
|
||||||
raise exceptions.UserError(_(
|
raise exceptions.UserError(_(
|
||||||
'''Problem connecting to Swift store, are the env variables
|
"Problem connecting to Swift store, are the env variables "
|
||||||
(SWIFT_HOST, SWIFT_ACCOUNT, SWIFT_PASSWORD) properly set ?
|
"(SWIFT_AUTH_URL, SWIFT_ACCOUNT, SWIFT_PASSWORD, "
|
||||||
'''))
|
"SWIFT_TENANT_NAME) properly set?"
|
||||||
|
))
|
||||||
try:
|
try:
|
||||||
conn = swiftclient.client.Connection(authurl=host,
|
conn = swiftclient.client.Connection(authurl=host,
|
||||||
user=account,
|
user=account,
|
||||||
key=password)
|
key=password,
|
||||||
|
tenant_name=tenant_name,
|
||||||
|
auth_version='2.0')
|
||||||
except ClientException:
|
except ClientException:
|
||||||
_logger.exception('Error connecting to Swift object store')
|
_logger.exception('Error connecting to Swift object store')
|
||||||
raise exceptions.UserError(_('Error on Swift connection'))
|
raise exceptions.UserError(_('Error on Swift connection'))
|
||||||
@@ -56,20 +63,18 @@ class IrAttachment(models.Model):
|
|||||||
swifturi.item())
|
swifturi.item())
|
||||||
read = base64.b64encode(obj_content)
|
read = base64.b64encode(obj_content)
|
||||||
except ClientException:
|
except ClientException:
|
||||||
|
read = ''
|
||||||
_logger.exception(
|
_logger.exception(
|
||||||
'Error reading object from Swift object store')
|
'Error reading object from Swift object store')
|
||||||
raise exceptions.UserError(_('Error reading on Swift'))
|
|
||||||
return read
|
return read
|
||||||
else:
|
else:
|
||||||
return super(IrAttachment, self)._store_file_read(fname, bin_size)
|
return super(IrAttachment, self)._store_file_read(fname, bin_size)
|
||||||
|
|
||||||
def _store_file_write(self, value, checksum):
|
def _store_file_write(self, key, bin_data):
|
||||||
if self._storage() == self._SWIFT_STORAGE:
|
if self._storage() == 'swift':
|
||||||
container = os.environ.get('SWIFT_WRITE_CONTAINER')
|
container = os.environ.get('SWIFT_WRITE_CONTAINER')
|
||||||
conn = self._get_swift_connection()
|
conn = self._get_swift_connection()
|
||||||
conn.put_container(container)
|
conn.put_container(container)
|
||||||
bin_data = value.decode('base64')
|
|
||||||
key = self._compute_checksum(bin_data)
|
|
||||||
filename = 'swift://{}/{}'.format(container, key)
|
filename = 'swift://{}/{}'.format(container, key)
|
||||||
try:
|
try:
|
||||||
conn.put_object(container, key, bin_data)
|
conn.put_object(container, key, bin_data)
|
||||||
@@ -78,7 +83,7 @@ class IrAttachment(models.Model):
|
|||||||
raise exceptions.UserError(_('Error writing to Swift'))
|
raise exceptions.UserError(_('Error writing to Swift'))
|
||||||
else:
|
else:
|
||||||
_super = super(IrAttachment, self)
|
_super = super(IrAttachment, self)
|
||||||
filename = _super._store_file_write(value, checksum)
|
filename = _super._store_file_write(key, bin_data)
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
@@ -86,6 +91,8 @@ class IrAttachment(models.Model):
|
|||||||
if fname.startswith('swift://'):
|
if fname.startswith('swift://'):
|
||||||
swifturi = SwiftUri(fname)
|
swifturi = SwiftUri(fname)
|
||||||
container = swifturi.container()
|
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'):
|
if container == os.environ.get('SWIFT_WRITE_CONTAINER'):
|
||||||
conn = self._get_swift_connection()
|
conn = self._get_swift_connection()
|
||||||
try:
|
try:
|
||||||
@@ -93,11 +100,7 @@ class IrAttachment(models.Model):
|
|||||||
except ClientException:
|
except ClientException:
|
||||||
_logger.exception(
|
_logger.exception(
|
||||||
_('Error deleting an object on the Swift store'))
|
_('Error deleting an object on the Swift store'))
|
||||||
raise exceptions.UserError(_('Error deleting on Swift'))
|
# we ignore the error, file will stay on the object
|
||||||
|
# storage but won't disrupt the process
|
||||||
else:
|
else:
|
||||||
super(IrAttachment, self)._file_delete(fname)
|
super(IrAttachment, self)._file_delete_from_store(fname)
|
||||||
|
|
||||||
def _get_stores(self):
|
|
||||||
l = [self._SWIFT_STORAGE]
|
|
||||||
l += super(IrAttachment, self)._get_stores()
|
|
||||||
return l
|
|
||||||
|
|||||||
@@ -87,10 +87,29 @@ class IrAttachment(models.Model):
|
|||||||
_super = super(IrAttachment, self)
|
_super = super(IrAttachment, self)
|
||||||
return _super._file_read(fname, bin_size=bin_size)
|
return _super._file_read(fname, bin_size=bin_size)
|
||||||
|
|
||||||
|
def _store_file_read(self, fname, bin_size=False):
|
||||||
|
storage = fname.partition('://')[0]
|
||||||
|
raise NotImplementedError(
|
||||||
|
'No implementation for %s' % (storage,)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _store_file_write(self, key, bin_data):
|
||||||
|
raise NotImplementedError(
|
||||||
|
'No implementation for %s' % (self.storage(),)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _store_file_delete(self, fname):
|
||||||
|
storage = fname.partition('://')[0]
|
||||||
|
raise NotImplementedError(
|
||||||
|
'No implementation for %s' % (storage,)
|
||||||
|
)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _file_write(self, value, checksum):
|
def _file_write(self, value, checksum):
|
||||||
if self._storage() in self._get_stores():
|
if self._storage() in self._get_stores():
|
||||||
filename = self._store_file_write(value, checksum)
|
bin_data = value.decode('base64')
|
||||||
|
key = self._compute_checksum(bin_data)
|
||||||
|
filename = self._store_file_write(key, bin_data)
|
||||||
else:
|
else:
|
||||||
filename = super(IrAttachment, self)._file_write(value, checksum)
|
filename = super(IrAttachment, self)._file_write(value, checksum)
|
||||||
return filename
|
return filename
|
||||||
@@ -99,6 +118,8 @@ class IrAttachment(models.Model):
|
|||||||
def _file_delete(self, fname):
|
def _file_delete(self, fname):
|
||||||
if self._is_file_from_a_store(fname):
|
if self._is_file_from_a_store(fname):
|
||||||
cr = self.env.cr
|
cr = self.env.cr
|
||||||
|
# using SQL to include files hidden through unlink or due to record
|
||||||
|
# rules
|
||||||
cr.execute("SELECT COUNT(*) FROM ir_attachment "
|
cr.execute("SELECT COUNT(*) FROM ir_attachment "
|
||||||
"WHERE store_fname = %s", (fname,))
|
"WHERE store_fname = %s", (fname,))
|
||||||
count = cr.fetchone()[0]
|
count = cr.fetchone()[0]
|
||||||
@@ -184,16 +205,21 @@ class IrAttachment(models.Model):
|
|||||||
storage = self._storage()
|
storage = self._storage()
|
||||||
if storage not in self._get_stores():
|
if storage not in self._get_stores():
|
||||||
return super(IrAttachment, self).force_storage()
|
return super(IrAttachment, self).force_storage()
|
||||||
|
self._force_storage_to_object_storage()
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _force_storage_to_object_storage(self, new_cr=False):
|
||||||
_logger.info('migrating files to the object storage')
|
_logger.info('migrating files to the object storage')
|
||||||
|
storage = self._storage()
|
||||||
domain = ['!', ('store_fname', '=like', '{}://%'.format(storage)),
|
domain = ['!', ('store_fname', '=like', '{}://%'.format(storage)),
|
||||||
'|',
|
'|',
|
||||||
('res_field', '=', False),
|
('res_field', '=', False),
|
||||||
('res_field', '!=', False)]
|
('res_field', '!=', False)]
|
||||||
# We do a copy of the environment so we can workaround the
|
# We do a copy of the environment so we can workaround the cache issue
|
||||||
# cache issue below. We do not create a new cursor because
|
# below. We do not create a new cursor by default because it causes
|
||||||
# it causes serialization issues due to concurrent updates on
|
# serialization issues due to concurrent updates on attachments during
|
||||||
# attachments during the installation
|
# the installation
|
||||||
with self.do_in_new_env() as new_env:
|
with self.do_in_new_env(new_cr=new_cr) as new_env:
|
||||||
model_env = new_env['ir.attachment']
|
model_env = new_env['ir.attachment']
|
||||||
ids = model_env.search(domain).ids
|
ids = model_env.search(domain).ids
|
||||||
for attachment_id in ids:
|
for attachment_id in ids:
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
Cloud Platform
|
Cloud Platform
|
||||||
==============
|
==============
|
||||||
|
|
||||||
Install addons required for the Camptocamp Cloud platform.
|
Install addons required for the Camptocamp Cloud platform, and that are
|
||||||
|
common to all platform providers.
|
||||||
|
|
||||||
* Provide a quick install that we can call at the setup / migration
|
* Provide a quick install that we can call at the setup / migration
|
||||||
of a database
|
of a database
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
'license': 'AGPL-3',
|
'license': 'AGPL-3',
|
||||||
'category': 'Extra Tools',
|
'category': 'Extra Tools',
|
||||||
'depends': [
|
'depends': [
|
||||||
'attachment_s3',
|
|
||||||
'session_redis',
|
'session_redis',
|
||||||
'monitoring_status',
|
'monitoring_status',
|
||||||
'logging_json',
|
'logging_json',
|
||||||
@@ -18,7 +17,7 @@
|
|||||||
'monitoring_statsd',
|
'monitoring_statsd',
|
||||||
'server_environment', # OCA/server-tools
|
'server_environment', # OCA/server-tools
|
||||||
],
|
],
|
||||||
'website': 'http://www.camptocamp.com',
|
'website': 'https://www.camptocamp.com',
|
||||||
'data': [],
|
'data': [],
|
||||||
'installable': True,
|
'installable': True,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ PlatformConfig = namedtuple(
|
|||||||
class FilestoreKind(object):
|
class FilestoreKind(object):
|
||||||
db = 'db'
|
db = 'db'
|
||||||
s3 = 's3' # or compatible s3 object storage
|
s3 = 's3' # or compatible s3 object storage
|
||||||
|
swift = 'swift'
|
||||||
file = 'file'
|
file = 'file'
|
||||||
|
|
||||||
|
|
||||||
@@ -37,26 +38,99 @@ class CloudPlatform(models.AbstractModel):
|
|||||||
_name = 'cloud.platform'
|
_name = 'cloud.platform'
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _config_by_server_env(self, environment):
|
def _platform_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
|
||||||
|
@api.model
|
||||||
|
def _config_by_server_env_for_exoscale(self):
|
||||||
configs = {
|
configs = {
|
||||||
'prod': PlatformConfig(filestore=FilestoreKind.s3),
|
'prod': PlatformConfig(filestore=FilestoreKind.s3),
|
||||||
'integration': PlatformConfig(filestore=FilestoreKind.s3),
|
'integration': PlatformConfig(filestore=FilestoreKind.s3),
|
||||||
'test': PlatformConfig(filestore=FilestoreKind.db),
|
'test': PlatformConfig(filestore=FilestoreKind.db),
|
||||||
'dev': PlatformConfig(filestore=FilestoreKind.db),
|
'dev': PlatformConfig(filestore=FilestoreKind.db),
|
||||||
}
|
}
|
||||||
return configs.get(environment) or configs['dev']
|
return configs
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _config_by_server_env(self, platform_kind, environment):
|
||||||
|
configs_getter = getattr(
|
||||||
|
self,
|
||||||
|
'_config_by_server_env_for_%s' % platform_kind,
|
||||||
|
None
|
||||||
|
)
|
||||||
|
configs = configs_getter() if configs_getter else {}
|
||||||
|
return configs.get(environment) or FilestoreKind.db
|
||||||
|
|
||||||
|
# Due to the addition of the ovh cloud platform
|
||||||
|
# This will be moved to cloud_platform_exoscale on v11
|
||||||
@api.model
|
@api.model
|
||||||
def install_exoscale(self):
|
def install_exoscale(self):
|
||||||
|
self.install('exoscale')
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def install(self, platform_kind):
|
||||||
|
assert platform_kind in self._platform_kinds()
|
||||||
params = self.env['ir.config_parameter'].sudo()
|
params = self.env['ir.config_parameter'].sudo()
|
||||||
params.set_param('cloud.platform.kind', 'exoscale')
|
params.set_param('cloud.platform.kind', platform_kind)
|
||||||
environment = config['running_env']
|
environment = config['running_env']
|
||||||
configs = self._config_by_server_env(environment)
|
configs = self._config_by_server_env(platform_kind, environment)
|
||||||
params.set_param('ir_attachment.location', configs.filestore)
|
params.set_param('ir_attachment.location', configs.filestore)
|
||||||
self.check()
|
self.check()
|
||||||
if configs.filestore == FilestoreKind.s3:
|
if configs.filestore in [FilestoreKind.swift, FilestoreKind.s3]:
|
||||||
self.env['ir.attachment'].sudo().force_storage()
|
self.env['ir.attachment'].sudo().force_storage()
|
||||||
_logger.info('cloud platform configured for exoscale')
|
_logger.info('cloud platform configured for {}'.format(platform_kind))
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _check_swift(self, environment_name):
|
||||||
|
params = self.env['ir.config_parameter'].sudo()
|
||||||
|
use_swift = (params.get_param('ir_attachment.location') ==
|
||||||
|
FilestoreKind.swift)
|
||||||
|
if environment_name in ('prod', 'integration'):
|
||||||
|
assert use_swift, (
|
||||||
|
"Swift must be used on production and integration instances. "
|
||||||
|
"It is activated, setting 'ir_attachment.location.' to 'swift'"
|
||||||
|
" The 'install_exoscale()' 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['SWIFT_WRITE_CONTAINER']
|
||||||
|
prod_container = bool(re.match(r'[a-z]+-odoo-prod',
|
||||||
|
container_name))
|
||||||
|
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_ovh()'."
|
||||||
|
)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _check_s3(self, environment_name):
|
def _check_s3(self, environment_name):
|
||||||
@@ -148,7 +222,10 @@ class CloudPlatform(models.AbstractModel):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
environment_name = config['running_env']
|
environment_name = config['running_env']
|
||||||
|
if kind == 'exoscale':
|
||||||
self._check_s3(environment_name)
|
self._check_s3(environment_name)
|
||||||
|
elif kind == 'ovh':
|
||||||
|
self._check_swift(environment_name)
|
||||||
self._check_redis(environment_name)
|
self._check_redis(environment_name)
|
||||||
|
|
||||||
@api.model_cr
|
@api.model_cr
|
||||||
|
|||||||
@@ -3,3 +3,7 @@
|
|||||||
|
|
||||||
def install_exoscale(ctx):
|
def install_exoscale(ctx):
|
||||||
ctx.env['cloud.platform'].install_exoscale()
|
ctx.env['cloud.platform'].install_exoscale()
|
||||||
|
|
||||||
|
|
||||||
|
def install_ovh(ctx):
|
||||||
|
ctx.env['cloud.platform'].install('ovh')
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Cloud Platform Exoscale
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Install addons specific to the Exoscale setup.
|
||||||
|
|
||||||
|
* The object storage is S3
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2017 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': '10.0.1.1.0',
|
||||||
|
'author': 'Camptocamp,Odoo Community Association (OCA)',
|
||||||
|
'license': 'AGPL-3',
|
||||||
|
'category': 'Extra Tools',
|
||||||
|
'depends': [
|
||||||
|
'cloud_platform',
|
||||||
|
'attachment_s3',
|
||||||
|
],
|
||||||
|
'website': 'https://www.camptocamp.com',
|
||||||
|
'data': [],
|
||||||
|
'installable': True,
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from . import cloud_platform
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2016 Camptocamp SA
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CloudPlatform(models.AbstractModel):
|
||||||
|
_inherit = 'cloud.platform'
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Cloud Platform OVH
|
||||||
|
==================
|
||||||
|
|
||||||
|
Install addons specific to the OVH setup.
|
||||||
|
|
||||||
|
* The object storage is Swift
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2017 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': '10.0.1.1.0',
|
||||||
|
'author': 'Camptocamp,Odoo Community Association (OCA)',
|
||||||
|
'license': 'AGPL-3',
|
||||||
|
'category': 'Extra Tools',
|
||||||
|
'depends': [
|
||||||
|
'cloud_platform',
|
||||||
|
'attachment_swift',
|
||||||
|
],
|
||||||
|
'website': 'https://www.camptocamp.com',
|
||||||
|
'data': [],
|
||||||
|
'installable': True,
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from . import cloud_platform
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2017 Camptocamp SA
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from odoo.addons.cloud_platform.models.cloud_platform import FilestoreKind
|
||||||
|
from odoo.addons.cloud_platform.models.cloud_platform import PlatformConfig
|
||||||
|
except ImportError:
|
||||||
|
FilestoreKind = None
|
||||||
|
PlatformConfig = None
|
||||||
|
_logger.debug("Cannot 'import from cloud_platform'")
|
||||||
|
|
||||||
|
|
||||||
|
class CloudPlatform(models.AbstractModel):
|
||||||
|
_inherit = 'cloud.platform'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _platform_kinds(self):
|
||||||
|
kinds = super(CloudPlatform, self)._platform_kinds()
|
||||||
|
kinds.append('ovh')
|
||||||
|
return kinds
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _config_by_server_env_for_ovh(self):
|
||||||
|
configs = {
|
||||||
|
'prod': PlatformConfig(filestore=FilestoreKind.swift),
|
||||||
|
'integration': PlatformConfig(filestore=FilestoreKind.swift),
|
||||||
|
'test': PlatformConfig(filestore=FilestoreKind.db),
|
||||||
|
'dev': PlatformConfig(filestore=FilestoreKind.db),
|
||||||
|
}
|
||||||
|
return configs
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def install_ovh(self):
|
||||||
|
self.install('ovh')
|
||||||
+2
-1
@@ -2,4 +2,5 @@ boto==2.42.0
|
|||||||
redis==2.10.5
|
redis==2.10.5
|
||||||
python-json-logger==0.1.5
|
python-json-logger==0.1.5
|
||||||
statsd==3.2.1
|
statsd==3.2.1
|
||||||
python-swiftclient==3.0.0
|
python-swiftclient==3.4.0
|
||||||
|
python-keystoneclient==3.13.0
|
||||||
|
|||||||
Reference in New Issue
Block a user