Add SERVER_ENV_CONFIG to configure vars from env. variable
Add SERVER_ENV_CONFIG_SECRET alongside SERVER_ENV_CONFIG Allows to isolate the secrets in your deployment Improve documentation regarding variables Add a server environment mixin To automatically convert fields into fields reading values from the environment. Until now, every module reimplements the same computed field. Read default values from database when no config is provided Automatically add <field_name>_env_default for every field transformed to a "computed from env" field, so a default value can be set. It will be used when the configuration is not set in a configuration file (when the key is absent, not empty). Allow to edit default values for env-computed fields When they don't have any key in the environment configuration files. In the UI, when a field is set in a configuration file, the field is readonly, if not the field is editable. Which means you can selectively choose which fields depend on the environment and which can use a "default" value stored in database. Fix a few small issues in mixin Use a dictionary to configure the fields Add global section Disable prefetch on env-computed fields As in the inverse field that write the value into the <field>_env_default we have to browse the record, the prefetch has the effect of calling compute on the env-computed field which resets the value to it's previous state before we have the occasion to store it. Use global section name as first part of the section Allow to edit all fields on creation Make server_environment_files optional Allow integration with keychain By adding options to change the compute and inverse methods for default fields Update documentation of server_environment, bump Add SERVER_ENV_CONFIG_SECRET alongside SERVER_ENV_CONFIG Allows to isolate the secrets in your deployment Reinforce server_environment base tests Add tests for the server env mixin Infer configparser getter from field type Fixes for review feedbacks Add tests and support of _inherits Use SavepointCase instead of TransactionCase It means less records to create for each test Fix iteration on records
This commit is contained in:
parent
04f1d2f4e3
commit
fee58c7681
|
|
@ -3,17 +3,20 @@
|
|||
:alt: License: GPL-3
|
||||
|
||||
==================
|
||||
server environment
|
||||
Server Environment
|
||||
==================
|
||||
|
||||
This module provides a way to define an environment in the main Odoo
|
||||
configuration file and to read some configurations from files
|
||||
depending on the configured environment: you define the environment in
|
||||
the main configuration file, and the values for the various possible
|
||||
environments are stored in the `server_environment_files` companion
|
||||
environments are stored in the ``server_environment_files`` companion
|
||||
module.
|
||||
|
||||
All the settings will be read only and visible under the Configuration
|
||||
The ``server_environment_files`` module is optional, the values can be set using
|
||||
an environment variable with a fallback on default values in the database.
|
||||
|
||||
The configuration read from the files are visible under the Configuration
|
||||
menu. If you are not in the 'dev' environment you will not be able to
|
||||
see the values contained in keys named '*passw*'.
|
||||
|
||||
|
|
@ -21,49 +24,128 @@ Installation
|
|||
============
|
||||
|
||||
By itself, this module does little. See for instance the
|
||||
`mail_environment` addon which depends on this one to allow configuring
|
||||
``mail_environment`` addon which depends on this one to allow configuring
|
||||
the incoming and outgoing mail servers depending on the environment.
|
||||
|
||||
To install this module, you need to provide a companion module called
|
||||
`server_environment_files`. You can copy and customize the provided
|
||||
`server_environment_files_sample` module for this purpose.
|
||||
You can store your configuration values in a companion module called
|
||||
``server_environment_files``. You can copy and customize the provided
|
||||
``server_environment_files_sample`` module for this purpose. Alternatively, you
|
||||
can provide them in environment variables ``SERVER_ENV_CONFIG`` and
|
||||
``SERVER_ENV_CONFIG_SECRET``.
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
To configure this module, you need to edit the main configuration file
|
||||
of your instance, and add a directive called `running_env`. Commonly
|
||||
of your instance, and add a directive called ``running_env``. Commonly
|
||||
used values are 'dev', 'test', 'production'::
|
||||
|
||||
[options]
|
||||
running_env=dev
|
||||
|
||||
You should then edit the settings you need in the
|
||||
`server_environment_files` addon. The
|
||||
`server_environment_files_sample` can be used as an example:
|
||||
Values associated to keys containing 'passw' are only displayed in the 'dev'
|
||||
environment.
|
||||
|
||||
You have several possibilities to set configuration values:
|
||||
|
||||
server_environment_files
|
||||
------------------------
|
||||
|
||||
You can edit the settings you need in the ``server_environment_files`` addon. The
|
||||
``server_environment_files_sample`` can be used as an example:
|
||||
|
||||
* values common to all / most environments can be stored in the
|
||||
`default/` directory using the .ini file syntax;
|
||||
``default/`` directory using the .ini file syntax;
|
||||
* each environment you need to define is stored in its own directory
|
||||
and can override or extend default values;
|
||||
* finally, you can override or extend values in the main configuration
|
||||
file of you instance.
|
||||
* you can override or extend values in the main configuration
|
||||
file of your instance;
|
||||
|
||||
Environment variable
|
||||
--------------------
|
||||
|
||||
You can define configuration in the environment variable ``SERVER_ENV_CONFIG``
|
||||
and/or ``SERVER_ENV_CONFIG_SECRET``. The 2 variables are handled the exact same
|
||||
way, this is only a convenience for the deployment where you can isolate the
|
||||
secrets in a different, encrypted, file. They are multi-line environment variables
|
||||
in the same configparser format than the files.
|
||||
If you used options in ``server_environment_files``, the options set in the
|
||||
environment variable overrides them.
|
||||
|
||||
The options in the environment variable are not dependent of ``running_env``,
|
||||
the content of the variable must be set accordingly to the running environment.
|
||||
|
||||
Example of setup:
|
||||
|
||||
A public file, containing that will contain public variables::
|
||||
|
||||
# These variables are not odoo standard variables,
|
||||
# they are there to represent what your file could look like
|
||||
export WORKERS='8'
|
||||
export MAX_CRON_THREADS='1'
|
||||
export LOG_LEVEL=info
|
||||
export LOG_HANDLER=":INFO"
|
||||
export DB_MAXCONN=5
|
||||
|
||||
# server environment options
|
||||
export SERVER_ENV_CONFIG="
|
||||
[storage_backend.my-sftp]
|
||||
sftp_server=10.10.10.10
|
||||
sftp_login=foo
|
||||
sftp_port=22200
|
||||
directory_path=Odoo
|
||||
"
|
||||
|
||||
A second file which is encrypted and contains secrets::
|
||||
|
||||
# This variable is not an odoo standard variable,
|
||||
# it is there to represent what your file could look like
|
||||
export DB_PASSWORD='xxxxxxxxx'
|
||||
# server environment options
|
||||
export SERVER_ENV_CONFIG_SECRET="
|
||||
[storage_backend.my-sftp]
|
||||
sftp_password=xxxxxxxxx
|
||||
"
|
||||
|
||||
Default values
|
||||
--------------
|
||||
|
||||
When using the ``server.env.mixin`` mixin, for each env-computed field, a
|
||||
companion field ``<field>_env_default`` is created. This field is not
|
||||
environment-dependent. It's a fallback value used when no key is set in
|
||||
configuration files / environment variable.
|
||||
|
||||
When the default field is used, the field is made editable on Odoo.
|
||||
|
||||
Note: empty environment keys always take precedence over default fields
|
||||
|
||||
|
||||
Keychain integration
|
||||
--------------------
|
||||
|
||||
Read the documentation of the class `models/server_env_mixin.py
|
||||
<models/server_env_mixin.py>`_.
|
||||
|
||||
Values associated to keys
|
||||
containing 'passw' are only displayed in the 'dev' environment.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
To use this module, in your code, you can follow this example::
|
||||
You can include a mixin in your model and configure the env-computed fields
|
||||
by an override of ``_server_env_fields``.
|
||||
|
||||
from openerp.addons.server_environment import serv_config
|
||||
for key, value in serv_config.items('external_service.ftp'):
|
||||
print (key, value)
|
||||
::
|
||||
|
||||
serv_config.get('external_service.ftp', 'tls')
|
||||
class StorageBackend(models.Model):
|
||||
_name = "storage.backend"
|
||||
_inherit = ["storage.backend", "server.env.mixin"]
|
||||
|
||||
@property
|
||||
def _server_env_fields(self):
|
||||
return {"directory_path": {}}
|
||||
|
||||
Read the documentation of the class and methods in `models/server_env_mixin.py
|
||||
<models/server_env_mixin.py>`__.
|
||||
|
||||
|
||||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||
|
|
|
|||
|
|
@ -17,4 +17,11 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
from . import models
|
||||
# TODO when migrating to 12, fix the import of serv_config by renaming the
|
||||
# file?
|
||||
# Add an alias to access to the 'serv_config' module as it is shadowed
|
||||
# in the following line by an import of a variable with the same name.
|
||||
# We can't change the import of serv_config for backward compatibility.
|
||||
from . import serv_config as server_env
|
||||
from .serv_config import serv_config, setboolean
|
||||
|
|
|
|||
|
|
@ -20,8 +20,11 @@
|
|||
|
||||
{
|
||||
"name": "server configuration environment files",
|
||||
"version": "11.0.1.0.1",
|
||||
"depends": ["base"],
|
||||
"version": "11.0.2.0.0",
|
||||
"depends": [
|
||||
"base",
|
||||
"base_sparse_field",
|
||||
],
|
||||
"author": "Camptocamp,Odoo Community Association (OCA)",
|
||||
"summary": "move some configurations out of the database",
|
||||
"website": "http://odoo-community.org/",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from . import server_env_mixin
|
||||
|
|
@ -0,0 +1,408 @@
|
|||
# Copyright 2018 Camptocamp (https://www.camptocamp.com).
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
|
||||
from functools import partialmethod
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from odoo import api, fields, models
|
||||
from ..serv_config import serv_config
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ServerEnvMixin(models.AbstractModel):
|
||||
"""Mixin to add server environment in existing models
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
::
|
||||
|
||||
class StorageBackend(models.Model):
|
||||
_name = "storage.backend"
|
||||
_inherit = ["storage.backend", "server.env.mixin"]
|
||||
|
||||
@property
|
||||
def _server_env_fields(self):
|
||||
return {"directory_path": {}}
|
||||
|
||||
With the snippet above, the "storage.backend" model now uses a server
|
||||
environment configuration for the field ``directory_path``.
|
||||
|
||||
Under the hood, this mixin automatically replaces the original field
|
||||
by an env-computed field that reads from the configuration files.
|
||||
|
||||
By default, it looks for the configuration in a section named
|
||||
``[model_name.Record Name]`` where ``model_name`` is the ``_name`` of the
|
||||
model with ``.`` replaced by ``_``. Then in a global section which is only
|
||||
the name of the model. They can be customized by overriding the method
|
||||
:meth:`~_server_env_section_name` and
|
||||
:meth:`~_server_env_global_section_name`.
|
||||
|
||||
For each field transformed to an env-computed field, a companion field
|
||||
``<field>_env_default`` is automatically created. When its value is set
|
||||
and the configuration files do not contain a key for that field, the
|
||||
env-computed field uses the default value stored in database. If there is a
|
||||
key for this field but it is empty, the env-computed field has an empty
|
||||
value.
|
||||
|
||||
Env-computed fields are conditionally editable, based on the absence
|
||||
of their key in environment configuration files. When edited, their
|
||||
value is stored in the database.
|
||||
|
||||
Integration with keychain
|
||||
-------------------------
|
||||
The keychain addon is used account information, encrypting the password
|
||||
with a key per environment.
|
||||
|
||||
The default behavior of server_environment is to store the default fields
|
||||
in a serialized field, so the password would lend there unencrypted.
|
||||
|
||||
You can benefit from keychain by using custom compute/inverse methods to
|
||||
get/set the password field:
|
||||
|
||||
::
|
||||
|
||||
class StorageBackend(models.Model):
|
||||
_name = 'storage.backend'
|
||||
_inherit = ['keychain.backend', 'collection.base']
|
||||
|
||||
@property
|
||||
def _server_env_fields(self):
|
||||
base_fields = super()._server_env_fields
|
||||
sftp_fields = {
|
||||
"sftp_server": {},
|
||||
"sftp_port": {},
|
||||
"sftp_login": {},
|
||||
"sftp_password": {
|
||||
"no_default_field": True,
|
||||
"compute_default": "_compute_password",
|
||||
"inverse_default": "_inverse_password",
|
||||
},
|
||||
}
|
||||
sftp_fields.update(base_fields)
|
||||
return sftp_fields
|
||||
|
||||
* ``no_default_field`` means that no new (sparse) field need to be
|
||||
created, it already is provided by keychain
|
||||
* ``compute_default`` is the name of the compute method to get the default
|
||||
value when no key is set in the configuration files.
|
||||
``_compute_password`` is implemented by ``keychain.backend``.
|
||||
* ``inverse_default`` is the name of the compute method to set the default
|
||||
value when it is editable. ``_inverse_password`` is implemented by
|
||||
``keychain.backend``.
|
||||
|
||||
"""
|
||||
_name = 'server.env.mixin'
|
||||
|
||||
server_env_defaults = fields.Serialized()
|
||||
|
||||
_server_env_getter_mapping = {
|
||||
'integer': 'getint',
|
||||
'float': 'getfloat',
|
||||
'monetary': 'getfloat',
|
||||
'boolean': 'getboolean',
|
||||
'char': 'get',
|
||||
'selection': 'get',
|
||||
'text': 'get',
|
||||
}
|
||||
|
||||
@property
|
||||
def _server_env_fields(self):
|
||||
"""Dict of fields to replace by fields computed from env
|
||||
|
||||
To override in models. The dictionary is:
|
||||
{'name_of_the_field': options}
|
||||
|
||||
Where ``options`` is a dictionary::
|
||||
|
||||
options = {
|
||||
"getter": "getint",
|
||||
"no_default_field": True,
|
||||
"compute_default": "_compute_password",
|
||||
"inverse_default": "_inverse_password",
|
||||
}
|
||||
|
||||
* ``getter``: The configparser getter can be one of: get, getboolean,
|
||||
getint, getfloat. The getter is automatically inferred from the
|
||||
type of the field, so it shouldn't generally be needed to set it.
|
||||
* ``no_default_field``: disable creation of a field for storing
|
||||
the default value, must be used with ``compute_default`` and
|
||||
``inverse_default``
|
||||
* ``compute_default``: name of a compute method to get the default
|
||||
value when no key is present in configuration files
|
||||
* ``inverse_default``: name of an inverse method to set the default
|
||||
value when the value is editable
|
||||
|
||||
Example::
|
||||
|
||||
@property
|
||||
def _server_env_fields(self):
|
||||
base_fields = super()._server_env_fields
|
||||
sftp_fields = {
|
||||
"sftp_server": {},
|
||||
"sftp_port": {},
|
||||
"sftp_login": {},
|
||||
"sftp_password": {},
|
||||
}
|
||||
sftp_fields.update(base_fields)
|
||||
return sftp_fields
|
||||
"""
|
||||
return {}
|
||||
|
||||
@api.model
|
||||
def _server_env_global_section_name(self):
|
||||
"""Name of the global section in the configuration files
|
||||
|
||||
Can be customized in your model
|
||||
"""
|
||||
return self._name.replace(".", "_")
|
||||
|
||||
@api.multi
|
||||
def _server_env_section_name(self):
|
||||
"""Name of the section in the configuration files
|
||||
|
||||
Can be customized in your model
|
||||
"""
|
||||
self.ensure_one()
|
||||
base = self._server_env_global_section_name()
|
||||
return ".".join((base, self.name))
|
||||
|
||||
@api.multi
|
||||
def _server_env_read_from_config(self, field_name, config_getter):
|
||||
self.ensure_one()
|
||||
global_section_name = self._server_env_global_section_name()
|
||||
section_name = self._server_env_section_name()
|
||||
try:
|
||||
# at this point we should have checked that we have a key with
|
||||
# _server_env_has_key_defined so we are sure that the value is
|
||||
# either in the global or the record config
|
||||
getter = getattr(serv_config, config_getter)
|
||||
if (section_name in serv_config
|
||||
and field_name in serv_config[section_name]):
|
||||
value = getter(section_name, field_name)
|
||||
else:
|
||||
value = getter(global_section_name, field_name)
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
"error trying to read field %s in section %s",
|
||||
field_name,
|
||||
section_name,
|
||||
)
|
||||
return False
|
||||
return value
|
||||
|
||||
@api.multi
|
||||
def _server_env_has_key_defined(self, field_name):
|
||||
self.ensure_one()
|
||||
global_section_name = self._server_env_global_section_name()
|
||||
section_name = self._server_env_section_name()
|
||||
has_global_config = (
|
||||
global_section_name in serv_config
|
||||
and field_name in serv_config[global_section_name]
|
||||
)
|
||||
has_config = (
|
||||
section_name in serv_config
|
||||
and field_name in serv_config[section_name]
|
||||
)
|
||||
return has_global_config or has_config
|
||||
|
||||
def _compute_server_env_from_config(self, field_name, options):
|
||||
getter_name = options.get('getter') if options else None
|
||||
if not getter_name:
|
||||
field_type = self._fields[field_name].type
|
||||
getter_name = self._server_env_getter_mapping.get(field_type)
|
||||
if not getter_name:
|
||||
# if you get this message and the field is working as expected,
|
||||
# you may want to add the type in _server_env_getter_mapping
|
||||
_logger.warning('server.env.mixin is used on a field of type %s, '
|
||||
'which may not be supported properly')
|
||||
getter_name = 'get'
|
||||
value = self._server_env_read_from_config(
|
||||
field_name, getter_name
|
||||
)
|
||||
self[field_name] = value
|
||||
|
||||
def _compute_server_env_from_default(self, field_name, options):
|
||||
if options and options.get('compute_default'):
|
||||
getattr(self, options['compute_default'])()
|
||||
else:
|
||||
default_field = self._server_env_default_fieldname(
|
||||
field_name
|
||||
)
|
||||
if default_field:
|
||||
self[field_name] = self[default_field]
|
||||
|
||||
@api.multi
|
||||
def _compute_server_env(self):
|
||||
"""Read values from environment configuration files
|
||||
|
||||
If an env-computed field has no key in configuration files,
|
||||
read from the ``<field>_env_default`` field from database.
|
||||
"""
|
||||
for record in self:
|
||||
for field_name, options in self._server_env_fields.items():
|
||||
if record._server_env_has_key_defined(field_name):
|
||||
record._compute_server_env_from_config(field_name, options)
|
||||
|
||||
else:
|
||||
record._compute_server_env_from_default(
|
||||
field_name, options
|
||||
)
|
||||
|
||||
def _inverse_server_env(self, field_name):
|
||||
options = self._server_env_fields[field_name]
|
||||
default_field = self._server_env_default_fieldname(field_name)
|
||||
is_editable_field = self._server_env_is_editable_fieldname(field_name)
|
||||
|
||||
for record in self:
|
||||
# when we write in an env-computed field, if it is editable
|
||||
# we update the default value in database
|
||||
|
||||
if record[is_editable_field]:
|
||||
if options and options.get('inverse_default'):
|
||||
getattr(record, options['inverse_default'])()
|
||||
elif default_field:
|
||||
record[default_field] = record[field_name]
|
||||
|
||||
@api.multi
|
||||
def _compute_server_env_is_editable(self):
|
||||
"""Compute <field>_is_editable values
|
||||
|
||||
We can edit an env-computed filed only if there is no key
|
||||
in any environment configuration file. If there is an empty
|
||||
key, it's an empty value so we can't edit the env-computed field.
|
||||
"""
|
||||
# we can't group it with _compute_server_env otherwise when called
|
||||
# in ``_inverse_server_env`` it would reset the value of the field
|
||||
for record in self:
|
||||
for field_name in self._server_env_fields:
|
||||
is_editable_field = self._server_env_is_editable_fieldname(
|
||||
field_name
|
||||
)
|
||||
is_editable = not record._server_env_has_key_defined(
|
||||
field_name
|
||||
)
|
||||
record[is_editable_field] = is_editable
|
||||
|
||||
def _server_env_view_set_readonly(self, view_arch):
|
||||
field_xpath = './/field[@name="%s"]'
|
||||
for field in self._server_env_fields:
|
||||
is_editable_field = self._server_env_is_editable_fieldname(field)
|
||||
for elem in view_arch.findall(field_xpath % field):
|
||||
# set env-computed fields to readonly if the configuration
|
||||
# files have a key set for this field
|
||||
elem.set('attrs',
|
||||
str({'readonly': [(is_editable_field, '=', False)]}))
|
||||
if not view_arch.findall(field_xpath % is_editable_field):
|
||||
# add the _is_editable fields in the view for the 'attrs'
|
||||
# domain
|
||||
view_arch.append(
|
||||
etree.Element(
|
||||
'field',
|
||||
name=is_editable_field,
|
||||
invisible="1"
|
||||
)
|
||||
)
|
||||
return view_arch
|
||||
|
||||
def _fields_view_get(self, view_id=None, view_type='form', toolbar=False,
|
||||
submenu=False):
|
||||
view_data = super()._fields_view_get(
|
||||
view_id=view_id, view_type=view_type,
|
||||
toolbar=toolbar, submenu=submenu
|
||||
)
|
||||
view_arch = etree.fromstring(view_data['arch'].encode('utf-8'))
|
||||
view_arch = self._server_env_view_set_readonly(view_arch)
|
||||
view_data['arch'] = etree.tostring(view_arch, encoding='unicode')
|
||||
return view_data
|
||||
|
||||
def _server_env_default_fieldname(self, base_field_name):
|
||||
"""Return the name of the field with default value"""
|
||||
options = self._server_env_fields[base_field_name]
|
||||
if options and options.get('no_default_field'):
|
||||
return ''
|
||||
return '%s_env_default' % (base_field_name,)
|
||||
|
||||
def _server_env_is_editable_fieldname(self, base_field_name):
|
||||
"""Return the name of the field for "is editable"
|
||||
|
||||
This is the field used to tell if the env-computed field can
|
||||
be edited.
|
||||
"""
|
||||
return '%s_env_is_editable' % (base_field_name,)
|
||||
|
||||
def _server_env_transform_field_to_read_from_env(self, field):
|
||||
"""Transform the original field in a computed field"""
|
||||
field.compute = '_compute_server_env'
|
||||
|
||||
inverse_method_name = '_inverse_server_env_%s' % field.name
|
||||
inverse_method = partialmethod(
|
||||
type(self)._inverse_server_env, field.name
|
||||
)
|
||||
setattr(type(self), inverse_method_name, inverse_method)
|
||||
field.inverse = inverse_method_name
|
||||
field.store = False
|
||||
field.required = False
|
||||
field.copy = False
|
||||
field.sparse = None
|
||||
field.prefetch = False
|
||||
|
||||
def _server_env_add_is_editable_field(self, base_field):
|
||||
"""Add a field indicating if we can edit the env-computed fields
|
||||
|
||||
It is used in the inverse function of the env-computed field
|
||||
and in the views to add 'readonly' on the fields.
|
||||
"""
|
||||
fieldname = self._server_env_is_editable_fieldname(base_field.name)
|
||||
# if the field is inherited, it's a related to its delegated model
|
||||
# (inherits), we want to override it with a new one
|
||||
if fieldname not in self._fields or self._fields[fieldname].inherited:
|
||||
field = fields.Boolean(
|
||||
compute='_compute_server_env_is_editable',
|
||||
automatic=True,
|
||||
# this is required to be able to edit fields
|
||||
# on new records
|
||||
default=True,
|
||||
)
|
||||
self._add_field(fieldname, field)
|
||||
|
||||
def _server_env_add_default_field(self, base_field):
|
||||
"""Add a field storing the default value
|
||||
|
||||
The default value is used when there is no key for an env-computed
|
||||
field in the configuration files.
|
||||
|
||||
The field is a sparse field stored in the serialized (json) field
|
||||
``server_env_defaults``.
|
||||
"""
|
||||
fieldname = self._server_env_default_fieldname(base_field.name)
|
||||
if not fieldname:
|
||||
return
|
||||
# if the field is inherited, it's a related to its delegated model
|
||||
# (inherits), we want to override it with a new one
|
||||
if fieldname not in self._fields or self._fields[fieldname].inherited:
|
||||
base_field_cls = base_field.__class__
|
||||
field_args = base_field.args.copy()
|
||||
field_args.pop('_sequence', None)
|
||||
field_args.update({
|
||||
'sparse': 'server_env_defaults',
|
||||
'automatic': True,
|
||||
})
|
||||
|
||||
if hasattr(base_field, 'selection'):
|
||||
field_args['selection'] = base_field.selection
|
||||
field = base_field_cls(**field_args)
|
||||
self._add_field(fieldname, field)
|
||||
|
||||
@api.model
|
||||
def _setup_base(self):
|
||||
super()._setup_base()
|
||||
for fieldname in self._server_env_fields:
|
||||
field = self._fields[fieldname]
|
||||
self._server_env_add_default_field(field)
|
||||
self._server_env_transform_field_to_read_from_env(field)
|
||||
self._server_env_add_is_editable_field(field)
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
import logging
|
||||
import os
|
||||
import configparser
|
||||
from lxml import etree
|
||||
|
|
@ -28,8 +29,17 @@ from odoo.tools.config import config as system_base_config
|
|||
|
||||
from .system_info import get_server_environment
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from odoo.addons import server_environment_files
|
||||
_dir = os.path.dirname(server_environment_files.__file__)
|
||||
except ImportError:
|
||||
_logger.info('not using server_environment_files for configuration,'
|
||||
' no directory found')
|
||||
_dir = None
|
||||
|
||||
ENV_VAR_NAMES = ('SERVER_ENV_CONFIG', 'SERVER_ENV_CONFIG_SECRET')
|
||||
|
||||
# Same dict as RawConfigParser._boolean_states
|
||||
_boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True,
|
||||
|
|
@ -44,6 +54,8 @@ if not system_base_config.get('running_env', False):
|
|||
"[options]\nrunning_env = dev"
|
||||
)
|
||||
|
||||
ck_path = None
|
||||
if _dir:
|
||||
ck_path = os.path.join(_dir, system_base_config['running_env'])
|
||||
|
||||
if not os.path.exists(ck_path):
|
||||
|
|
@ -80,8 +92,7 @@ def _listconf(env_path):
|
|||
return files
|
||||
|
||||
|
||||
def _load_config():
|
||||
"""Load the configuration and return a ConfigParser instance."""
|
||||
def _load_config_from_server_env_files(config_p):
|
||||
default = os.path.join(_dir, 'default')
|
||||
running_env = os.path.join(_dir,
|
||||
system_base_config['running_env'])
|
||||
|
|
@ -90,16 +101,40 @@ def _load_config():
|
|||
else:
|
||||
conf_files = _listconf(running_env)
|
||||
|
||||
config_p = configparser.SafeConfigParser()
|
||||
# options are case-sensitive
|
||||
config_p.optionxform = str
|
||||
try:
|
||||
config_p.read(conf_files)
|
||||
except Exception as e:
|
||||
raise Exception('Cannot read config files "%s": %s' % (conf_files, e))
|
||||
|
||||
|
||||
def _load_config_from_rcfile(config_p):
|
||||
config_p.read(system_base_config.rcfile)
|
||||
config_p.remove_section('options')
|
||||
|
||||
|
||||
def _load_config_from_env(config_p):
|
||||
for varname in ENV_VAR_NAMES:
|
||||
env_config = os.getenv(varname)
|
||||
if env_config:
|
||||
try:
|
||||
config_p.read_string(env_config)
|
||||
except configparser.Error as err:
|
||||
raise Exception(
|
||||
'%s content could not be parsed: %s'
|
||||
% (varname, err,)
|
||||
)
|
||||
|
||||
|
||||
def _load_config():
|
||||
"""Load the configuration and return a ConfigParser instance."""
|
||||
config_p = configparser.SafeConfigParser()
|
||||
# options are case-sensitive
|
||||
config_p.optionxform = str
|
||||
|
||||
if _dir:
|
||||
_load_config_from_server_env_files(config_p)
|
||||
_load_config_from_rcfile(config_p)
|
||||
_load_config_from_env(config_p)
|
||||
return config_p
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -18,3 +18,4 @@
|
|||
#
|
||||
##############################################################################
|
||||
from . import test_server_environment
|
||||
from . import test_environment_variable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
# Copyright 2018 Camptocamp (https://www.camptocamp.com).
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.tests import common
|
||||
from odoo.addons.server_environment import server_env
|
||||
from odoo.tools.config import config
|
||||
|
||||
import odoo.addons.server_environment.models.server_env_mixin as \
|
||||
server_env_mixin
|
||||
|
||||
|
||||
class ServerEnvironmentCase(common.SavepointCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self._original_running_env = config.get('running_env')
|
||||
config['running_env'] = 'testing'
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
config['running_env'] = self._original_running_env
|
||||
|
||||
@contextmanager
|
||||
def set_config_dir(self, path):
|
||||
original_dir = server_env._dir
|
||||
if path and not os.path.isabs(path):
|
||||
path = os.path.join(os.path.dirname(__file__,), path)
|
||||
server_env._dir = path
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
server_env._dir = original_dir
|
||||
|
||||
@contextmanager
|
||||
def set_env_variables(self, public=None, secret=None):
|
||||
newkeys = {}
|
||||
if public:
|
||||
newkeys['SERVER_ENV_CONFIG'] = public
|
||||
if secret:
|
||||
newkeys['SERVER_ENV_CONFIG_SECRET'] = secret
|
||||
with patch.dict('os.environ', newkeys):
|
||||
yield
|
||||
|
||||
@contextmanager
|
||||
def load_config(self, public=None, secret=None):
|
||||
original_serv_config = server_env_mixin.serv_config
|
||||
try:
|
||||
with self.set_config_dir(None), \
|
||||
self.set_env_variables(public, secret):
|
||||
parser = server_env._load_config()
|
||||
server_env_mixin.serv_config = parser
|
||||
yield
|
||||
|
||||
finally:
|
||||
server_env_mixin.serv_config = original_serv_config
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# Copyright 2018 Camptocamp (https://www.camptocamp.com).
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
from odoo.addons.server_environment import server_env
|
||||
from .common import ServerEnvironmentCase
|
||||
|
||||
|
||||
class TestEnvironmentVariables(ServerEnvironmentCase):
|
||||
|
||||
def test_env_variables(self):
|
||||
public = (
|
||||
"[section]\n"
|
||||
"foo=bar\n"
|
||||
"bar=baz\n"
|
||||
)
|
||||
secret = (
|
||||
"[section]\n"
|
||||
"bar=foo\n"
|
||||
"alice=bob\n"
|
||||
)
|
||||
with self.set_config_dir(None), \
|
||||
self.set_env_variables(public, secret):
|
||||
parser = server_env._load_config()
|
||||
self.assertEqual(
|
||||
list(parser.keys()),
|
||||
['DEFAULT', 'section']
|
||||
)
|
||||
self.assertDictEqual(
|
||||
dict(parser['section'].items()),
|
||||
{'alice': 'bob',
|
||||
'bar': 'foo',
|
||||
'foo': 'bar'}
|
||||
)
|
||||
|
||||
def test_env_variables_override(self):
|
||||
public = (
|
||||
"[external_service.ftp]\n"
|
||||
"user=foo\n"
|
||||
)
|
||||
with self.set_config_dir('testfiles'), \
|
||||
self.set_env_variables(public):
|
||||
parser = server_env._load_config()
|
||||
val = parser.get('external_service.ftp', 'user')
|
||||
self.assertEqual(val, 'foo')
|
||||
|
|
@ -17,11 +17,11 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
from odoo.tests import common
|
||||
from odoo.addons.server_environment import serv_config
|
||||
from odoo.addons.server_environment import server_env
|
||||
from . import common
|
||||
|
||||
|
||||
class TestEnv(common.TransactionCase):
|
||||
class TestEnv(common.ServerEnvironmentCase):
|
||||
|
||||
def test_view(self):
|
||||
model = self.env['server.config']
|
||||
|
|
@ -43,5 +43,9 @@ class TestEnv(common.TransactionCase):
|
|||
self.assertTrue(pass_checked)
|
||||
|
||||
def test_value_retrival(self):
|
||||
val = serv_config.get('external_service.ftp', 'user')
|
||||
self.assertEqual(val, 'toto')
|
||||
with self.set_config_dir('testfiles'):
|
||||
parser = server_env._load_config()
|
||||
val = parser.get('external_service.ftp', 'user')
|
||||
self.assertEqual(val, 'testing')
|
||||
val = parser.get('external_service.ftp', 'host')
|
||||
self.assertEqual(val, 'sftp.example.com')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
[external_service.ftp]
|
||||
host = sftp.example.com
|
||||
user = foo
|
||||
password = bar
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[external_service.ftp]
|
||||
user = testing
|
||||
Loading…
Reference in New Issue