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:
Guewen Baconnier
2017-09-21 09:03:46 +02:00
committed by GitHub
co-authored by GitHub
22 changed files with 366 additions and 275 deletions
+49 -8
View File
@@ -12,7 +12,15 @@ On the platform we want to achieve having:
* 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
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
@@ -21,23 +29,30 @@ a S3 compatible one, and we store the werkzeug sessions on Redis.
Libraries that must be added in ``requirements.txt``:
```
boto==2.42.0
redis==2.10.5
python-json-logger==0.1.5
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
The `--load` option of Odoo must contains the following addons:
* `attachment_s3`
* `attachment_s3` or `attachment_swift` depending of the provider used.
* `session_redis`
* `logging_json`
Example:
`--load=web,web_kanban,attachment_s3,session_redis,logging_json`
`--load=web,web_kanban,attachment_swift,session_redis,logging_json`
### 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.
### Attachments in the Object Storage
### Attachments in the Object Storage S3
* prod: stored RW in the object storage
* `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.
* `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
* prod:
@@ -108,11 +142,18 @@ Should be active at least on the production server
### Automatic Configuration
Calling `ctx.env['cloud.platform'].install_exoscale()` in an
`anthem` song will configure some parameters such as the
`ir_attachment.location` and migrate the existing attachments to the
object storage.
An automatic configuration can be executed from an `anthem` song to configure
some parameters such as the `ir_attachment.location` and migrate the existing
attachments to the 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
+2 -2
View File
@@ -9,11 +9,11 @@
'author': 'Camptocamp,Odoo Community Association (OCA)',
'license': 'AGPL-3',
'category': 'Knowledge Management',
'depends': ['base'],
'depends': ['base', 'base_attachment_object_storage'],
'external_dependencies': {
'python': ['boto'],
},
'website': 'http://www.camptocamp.com',
'website': 'https://www.camptocamp.com',
'data': [],
'installable': True,
}
+17 -206
View File
@@ -7,12 +7,8 @@ import base64
import logging
import os
import xml.dom.minidom
from contextlib import closing, contextmanager
from functools import partial
import psycopg2
import odoo
from odoo import _, api, exceptions, models
from ..s3uri import S3Uri
@@ -30,67 +26,10 @@ except ImportError:
class IrAttachment(models.Model):
_inherit = "ir.attachment"
@api.multi
def _store_in_db_when_s3(self):
""" Return whether an attachment must be stored in db
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()
def _get_stores(self):
l = ['s3']
l += super(IrAttachment, self)._get_stores()
return l
@api.model
def _get_s3_bucket(self, name=None):
@@ -157,7 +96,8 @@ class IrAttachment(models.Model):
return msg
@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)
try:
bucket = self._get_s3_bucket(name=s3uri.bucket())
@@ -171,24 +111,17 @@ class IrAttachment(models.Model):
read = base64.b64encode(filekey.get_contents_as_string())
else:
read = ''
_logger.info("attachment '%s' missing on object storage", fname)
_logger.info(
"attachment '%s' missing on object storage", fname
)
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:
_super = super(IrAttachment, self)
return _super._file_read(fname, bin_size=bin_size)
return super(IrAttachment, self)._store_file_read(fname, bin_size)
@api.model
def _file_write(self, value, checksum):
storage = self._storage()
if storage == 's3':
def _store_file_write(self, key, bin_data):
if self._storage() == 's3':
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)
filename = 's3://%s/%s' % (bucket.name, key)
try:
@@ -203,18 +136,13 @@ class IrAttachment(models.Model):
(self._parse_s3_error(error),)
)
else:
filename = super(IrAttachment, self)._file_write(value, checksum)
_super = super(IrAttachment, self)
filename = _super._store_file_write(key, bin_data)
return filename
@api.model
def _file_delete(self, fname):
def _store_file_delete(self, fname):
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)
bucket_name = s3uri.bucket()
item_name = s3uri.item()
@@ -223,7 +151,7 @@ class IrAttachment(models.Model):
if bucket_name == os.environ.get('AWS_BUCKETNAME'):
bucket = self._get_s3_bucket()
filekey = bucket.get_key(item_name)
if not count and filekey:
if filekey:
try:
filekey.delete()
_logger.info(
@@ -236,121 +164,4 @@ class IrAttachment(models.Model):
'Error during deletion of the file %s' % fname
)
else:
super(IrAttachment, self)._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()
super(IrAttachment, self)._store_file_delete(fname)
+23 -6
View File
@@ -1,23 +1,22 @@
Attachments on Swift storage
============================
This addon allows to store the attachments (documents and assets) on
OpenStack Object Storage (Swift)
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``
and the value in the form ``swift``.
* Create or set the system parameter with the key ``ir_attachment.location`` with the following value ``swift``.
Configure accesses with environment variables:
* ``SWIFT_HOST``
* ``SWIFT_AUTH_URL`` : URL of the Swift server
* ``SWIFT_TENANT_NAME``
* ``SWIFT_ACCOUNT``
* ``SWIFT_PASSWORD``
* ``SWIFT_WRITE_CONTAINER``
* ``SWIFT_WRITE_CONTAINER`` : Name of the container to use in the store (created if not existing)
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):
``--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
+5 -3
View File
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Camptocamp SA
# Copyright 2017 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
@@ -11,9 +11,11 @@
'category': 'Knowledge Management',
'depends': ['base_attachment_object_storage'],
'external_dependencies': {
'python': ['swiftclient'],
'python': ['swiftclient',
'keystoneclient',
],
},
'website': 'http://www.camptocamp.com',
'website': 'https://www.camptocamp.com',
'data': [],
'installable': True,
}
+23 -20
View File
@@ -24,23 +24,30 @@ except ImportError:
class IrAttachment(models.Model):
_inherit = 'ir.attachment'
_SWIFT_STORAGE = 'swift'
def _get_stores(self):
l = ['swift']
l += super(IrAttachment, self)._get_stores()
return l
@api.model
def _get_swift_connection(self):
""" 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')
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(_(
'''Problem connecting to Swift store, are the env variables
(SWIFT_HOST, SWIFT_ACCOUNT, SWIFT_PASSWORD) properly set ?
'''))
"Problem connecting to Swift store, are the env variables "
"(SWIFT_AUTH_URL, SWIFT_ACCOUNT, SWIFT_PASSWORD, "
"SWIFT_TENANT_NAME) properly set?"
))
try:
conn = swiftclient.client.Connection(authurl=host,
user=account,
key=password)
key=password,
tenant_name=tenant_name,
auth_version='2.0')
except ClientException:
_logger.exception('Error connecting to Swift object store')
raise exceptions.UserError(_('Error on Swift connection'))
@@ -56,20 +63,18 @@ class IrAttachment(models.Model):
swifturi.item())
read = base64.b64encode(obj_content)
except ClientException:
read = ''
_logger.exception(
'Error reading object from Swift object store')
raise exceptions.UserError(_('Error reading on Swift'))
return read
else:
return super(IrAttachment, self)._store_file_read(fname, bin_size)
def _store_file_write(self, value, checksum):
if self._storage() == self._SWIFT_STORAGE:
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)
bin_data = value.decode('base64')
key = self._compute_checksum(bin_data)
filename = 'swift://{}/{}'.format(container, key)
try:
conn.put_object(container, key, bin_data)
@@ -78,7 +83,7 @@ class IrAttachment(models.Model):
raise exceptions.UserError(_('Error writing to Swift'))
else:
_super = super(IrAttachment, self)
filename = _super._store_file_write(value, checksum)
filename = _super._store_file_write(key, bin_data)
return filename
@api.model
@@ -86,6 +91,8 @@ class IrAttachment(models.Model):
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:
@@ -93,11 +100,7 @@ class IrAttachment(models.Model):
except ClientException:
_logger.exception(
_('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:
super(IrAttachment, self)._file_delete(fname)
def _get_stores(self):
l = [self._SWIFT_STORAGE]
l += super(IrAttachment, self)._get_stores()
return l
super(IrAttachment, self)._file_delete_from_store(fname)
@@ -87,10 +87,29 @@ class IrAttachment(models.Model):
_super = super(IrAttachment, self)
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
def _file_write(self, value, checksum):
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:
filename = super(IrAttachment, self)._file_write(value, checksum)
return filename
@@ -99,6 +118,8 @@ class IrAttachment(models.Model):
def _file_delete(self, fname):
if self._is_file_from_a_store(fname):
cr = self.env.cr
# using SQL to include files hidden through unlink or due to record
# rules
cr.execute("SELECT COUNT(*) FROM ir_attachment "
"WHERE store_fname = %s", (fname,))
count = cr.fetchone()[0]
@@ -184,16 +205,21 @@ class IrAttachment(models.Model):
storage = self._storage()
if storage not in self._get_stores():
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')
storage = self._storage()
domain = ['!', ('store_fname', '=like', '{}://%'.format(storage)),
'|',
('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() as new_env:
# We do a copy of the environment so we can workaround the cache issue
# below. We do not create a new cursor by default 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:
model_env = new_env['ir.attachment']
ids = model_env.search(domain).ids
for attachment_id in ids:
+2 -1
View File
@@ -1,7 +1,8 @@
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
of a database
+1 -2
View File
@@ -10,7 +10,6 @@
'license': 'AGPL-3',
'category': 'Extra Tools',
'depends': [
'attachment_s3',
'session_redis',
'monitoring_status',
'logging_json',
@@ -18,7 +17,7 @@
'monitoring_statsd',
'server_environment', # OCA/server-tools
],
'website': 'http://www.camptocamp.com',
'website': 'https://www.camptocamp.com',
'data': [],
'installable': True,
}
+83 -6
View File
@@ -30,6 +30,7 @@ PlatformConfig = namedtuple(
class FilestoreKind(object):
db = 'db'
s3 = 's3' # or compatible s3 object storage
swift = 'swift'
file = 'file'
@@ -37,26 +38,99 @@ class CloudPlatform(models.AbstractModel):
_name = 'cloud.platform'
@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 = {
'prod': PlatformConfig(filestore=FilestoreKind.s3),
'integration': PlatformConfig(filestore=FilestoreKind.s3),
'test': 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
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.set_param('cloud.platform.kind', 'exoscale')
params.set_param('cloud.platform.kind', platform_kind)
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)
self.check()
if configs.filestore == FilestoreKind.s3:
if configs.filestore in [FilestoreKind.swift, FilestoreKind.s3]:
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
def _check_s3(self, environment_name):
@@ -148,7 +222,10 @@ class CloudPlatform(models.AbstractModel):
)
return
environment_name = config['running_env']
if kind == 'exoscale':
self._check_s3(environment_name)
elif kind == 'ovh':
self._check_swift(environment_name)
self._check_redis(environment_name)
@api.model_cr
+4
View File
@@ -3,3 +3,7 @@
def install_exoscale(ctx):
ctx.env['cloud.platform'].install_exoscale()
def install_ovh(ctx):
ctx.env['cloud.platform'].install('ovh')
+6
View File
@@ -0,0 +1,6 @@
Cloud Platform Exoscale
=======================
Install addons specific to the Exoscale setup.
* The object storage is S3
+1
View File
@@ -0,0 +1 @@
from . import models
+19
View File
@@ -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'
+7
View File
@@ -0,0 +1,7 @@
Cloud Platform OVH
==================
Install addons specific to the OVH setup.
* The object storage is Swift
+1
View File
@@ -0,0 +1 @@
from . import models
+19
View File
@@ -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,
}
+1
View File
@@ -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
View File
@@ -2,4 +2,5 @@ boto==2.42.0
redis==2.10.5
python-json-logger==0.1.5
statsd==3.2.1
python-swiftclient==3.0.0
python-swiftclient==3.4.0
python-keystoneclient==3.13.0