diff --git a/cloud_platform/models/cloud_platform.py b/cloud_platform/models/cloud_platform.py index 06de6d3..44026f2 100644 --- a/cloud_platform/models/cloud_platform.py +++ b/cloud_platform/models/cloud_platform.py @@ -43,6 +43,12 @@ class CloudPlatform(osv.osv_abstract): # it in cloud_platform_exoscale in V11 return ['exoscale'] + def _filestore_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 def _config_by_server_env_for_exoscale(self): @@ -220,6 +226,86 @@ class CloudPlatform(osv.osv_abstract): "automatically set by the function 'install_exoscale()'." ) + def _check_azure(self, cr, uid, environment_name, context=None): + params = self.env["ir.config_parameter"].sudo() + use_azure = params.get_param("ir_attachment.location") == AZURE_STORE_KIND.name + if environment_name in ("prod", "integration"): + # Labs instances use azure by default, but we don't want + # to enforce it in case we want to test something with a different + # storage. At your own risks! + assert use_azure, ( + "azure must be used on production and integration instances. " + "It is activated by setting 'ir_attachment.location.' to 'azure'." + " The 'install()' function sets this option " + "automatically." + ) + if use_azure: + key_sets = [ + ["AZURE_STORAGE_USE_AAD", "AZURE_STORAGE_ACCOUNT_URL"], + ["AZURE_STORAGE_CONNECTION_STRING"], + [ + "AZURE_STORAGE_ACCOUNT_NAME", + "AZURE_STORAGE_ACCOUNT_URL", + "AZURE_STORAGE_ACCOUNT_KEY", + ], + ] + is_valid = False + for key_set in key_sets: + if all([os.environ.get(key) for key in key_set]): + is_valid = True + break + assert is_valid, ( + "When ir_attachment.location is set to 'azure', " + "at least one of the following enviromnent variable set " + "is required : {}".format( + " or ".join( + [" + ".join([key for key in key_set]) for key_set in key_sets] + ) + ) + ) + storage_name = os.environ.get("AZURE_STORAGE_NAME", "") + if environment_name in ("prod", "integration", "labs"): + assert storage_name, ( + "AZURE_STORAGE_NAME environment variable is required when " + "ir_attachment.location is 'azure'.\n" + "Normally, 'azure' is activated on labs, integration " + "and production, but should not be used in dev environment" + " (or using a dedicated dev bucket, never using the " + "integration/prod bucket).\n" + "If you don't actually need a bucket, change the" + " 'ir_attachment.location' parameter." + ) + # A bucket name is defined under the following format + # ^[a-z]+\-[a-z]+\-\d+$ + # Anything other than prod bucket must be suffixed with env name + # + # Use AZURE_STORAGE_NAME_UNSTRUCTURED to by-pass check + # on bucket name structure + if os.environ.get("AZURE_STORAGE_NAME_UNSTRUCTURED"): + return + prod_bucket = bool(re.match(r"^[a-z]+\-[a-z]+\-\d+$", storage_name)) + if environment_name == "prod": + assert prod_bucket, ( + "AZURE_STORAGE_NAME should match '^[a-z]+\\-[a-z]+\\-\\d+$', " + "we got: '%s'" % (storage_name,) + ) + else: + # if we are using the prod bucket on another instance + # such as an integration, we must be sure to be in read only! + assert not prod_bucket, ( + "AZURE_STORAGE_NAME should not match '^[a-z]+\\-[a-z]+\\-\\d+$', " + "we got: '%s'" % (storage_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()'." + ) + + def _check_redis(self, cr, uid, environment_name, context=None): if environment_name in ('prod', 'integration', 'labs', 'test'): assert is_true(os.environ.get('ODOO_SESSION_REDIS')), ( @@ -262,6 +348,8 @@ class CloudPlatform(osv.osv_abstract): self._check_s3(cr, uid, environment_name, context) elif kind == 'ovh': self._check_swift(cr, uid, environment_name, context) + elif kind == 'azure': + self._check_azure(cr, uid, environment_name, context) self._check_redis(cr, uid, environment_name, context) def _register_hook(self, cr): diff --git a/cloud_platform_azure/README.md b/cloud_platform_azure/README.md new file mode 100644 index 0000000..1f7bd5d --- /dev/null +++ b/cloud_platform_azure/README.md @@ -0,0 +1,6 @@ +Cloud Platform Azure +==================== + +Install addons specific to the Azure setup. + + * The object storage is Azure blob storage diff --git a/cloud_platform_azure/__init__.py b/cloud_platform_azure/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/cloud_platform_azure/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/cloud_platform_azure/__openerp__.py b/cloud_platform_azure/__openerp__.py new file mode 100644 index 0000000..5cfa373 --- /dev/null +++ b/cloud_platform_azure/__openerp__.py @@ -0,0 +1,24 @@ +# Copyright 2017-2021 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + + +{ + "name": "Cloud Platform Azure", + "summary": "Addons required for the Camptocamp Cloud Platform on Azure", + "version": "15.0.1.0.0", + "author": "Camptocamp,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Extra Tools", + "depends": [ + "cloud_platform", + "attachment_azure", + "monitoring_prometheus", + ], + "excludes": [ + "cloud_platform_ovh", + "cloud_platform_exoscale", + ], + "website": "https://www.camptocamp.com", + "data": [], + "installable": True, +} diff --git a/cloud_platform_azure/models/__init__.py b/cloud_platform_azure/models/__init__.py new file mode 100644 index 0000000..5d08f36 --- /dev/null +++ b/cloud_platform_azure/models/__init__.py @@ -0,0 +1 @@ +from . import cloud_platform diff --git a/cloud_platform_azure/models/cloud_platform.py b/cloud_platform_azure/models/cloud_platform.py new file mode 100644 index 0000000..472ebc6 --- /dev/null +++ b/cloud_platform_azure/models/cloud_platform.py @@ -0,0 +1,39 @@ +# Copyright 2016-2021 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import re +import os + +from openerp.osv import osv +from openerp.addons.cloud_platform.models.cloud_platform import FilestoreKind +from openerp.addons.cloud_platform.models.cloud_platform import PlatformConfig + +AZURE_STORE_KIND = FilestoreKind("azure", "remote") + + +class CloudPlatform(osv.osv): + _inherit = "cloud.platform" + + def _filestore_kinds(self): + kinds = super(CloudPlatform, self)._filestore_kinds() + kinds["azure"] = AZURE_STORE_KIND + return kinds + + def _platform_kinds(self): + kinds = super(CloudPlatform, self)._platform_kinds() + kinds.append("azure") + return kinds + + def _config_by_server_env_for_azure(self): + fs_kinds = self._filestore_kinds() + configs = { + "prod": PlatformConfig(filestore=fs_kinds["azure"]), + "integration": PlatformConfig(filestore=fs_kinds["azure"]), + "labs": PlatformConfig(filestore=fs_kinds["azure"]), + "test": PlatformConfig(filestore=fs_kinds["db"]), + "dev": PlatformConfig(filestore=fs_kinds["db"]), + } + return configs + + def install_azure(self, cr, uid, context=None): + self.install(cr, uid, "azure", context) diff --git a/monitoring_prometheus/README.rst b/monitoring_prometheus/README.rst new file mode 100644 index 0000000..aa98ffe --- /dev/null +++ b/monitoring_prometheus/README.rst @@ -0,0 +1,17 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :alt: License + +============================== +Monitoring: Prometheus metrics +============================== + +Add an endpoint */metrics* to allow a Prometheus server to fetch application metrics. +Current available metrics are: + +* Request completion time with 3 differentiators: + * Filestore + * Assets + * Everything else +* Longpolling request count + +No additional configuration is needed, just ensure that the Prometheus server is allowed to communicate with Odoo diff --git a/monitoring_prometheus/__init__.py b/monitoring_prometheus/__init__.py new file mode 100644 index 0000000..91c5580 --- /dev/null +++ b/monitoring_prometheus/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/monitoring_prometheus/__openerp__.py b/monitoring_prometheus/__openerp__.py new file mode 100644 index 0000000..260570c --- /dev/null +++ b/monitoring_prometheus/__openerp__.py @@ -0,0 +1,22 @@ +# Copyright 2016-2021 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + + +{ + "name": "Monitoring: Prometheus Metrics", + "version": "15.0.1.0.0", + "author": "Camptocamp,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "category", + "depends": [ + "base", + "web", + "server_environment", + ], + "website": "http://www.camptocamp.com", + "data": [], + "external_dependencies": { + "python": ["prometheus_client"], + }, + "installable": True, +} diff --git a/monitoring_prometheus/controllers/__init__.py b/monitoring_prometheus/controllers/__init__.py new file mode 100644 index 0000000..13ff72f --- /dev/null +++ b/monitoring_prometheus/controllers/__init__.py @@ -0,0 +1 @@ +from . import prometheus_metrics diff --git a/monitoring_prometheus/controllers/prometheus_metrics.py b/monitoring_prometheus/controllers/prometheus_metrics.py new file mode 100644 index 0000000..fae6d36 --- /dev/null +++ b/monitoring_prometheus/controllers/prometheus_metrics.py @@ -0,0 +1,11 @@ +# Copyright 2016-2021 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from openerp.http import Controller, route +from prometheus_client import generate_latest + + +class PrometheusController(Controller): + @route("/metrics", auth="public") + def metrics(self): + return generate_latest() diff --git a/monitoring_prometheus/models/__init__.py b/monitoring_prometheus/models/__init__.py new file mode 100644 index 0000000..9a5eb71 --- /dev/null +++ b/monitoring_prometheus/models/__init__.py @@ -0,0 +1 @@ +from . import ir_http diff --git a/monitoring_prometheus/models/ir_http.py b/monitoring_prometheus/models/ir_http.py new file mode 100644 index 0000000..67d76ad --- /dev/null +++ b/monitoring_prometheus/models/ir_http.py @@ -0,0 +1,40 @@ +# Copyright 2016-2021 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from openerp import osv +from openerp.http import request +from prometheus_client import Summary, Counter + + +REQUEST_TIME = Summary( + "request_latency_sec", "Request response time in sec", ["query_type"] +) +LONGPOLLING_COUNT = Counter("longpolling", "Longpolling request count") + + +class IrHttp(osv.osv_abstract): + _inherit = "ir.http" + + @classmethod + def _dispatch(cls): + path_info = request.httprequest.environ.get("PATH_INFO") + + if path_info.startswith("/longpolling/"): + LONGPOLLING_COUNT.inc() + return super(IrHttp, self)._dispatch() + + if path_info.startswith("/metrics"): + return super(IrHttp, self)._dispatch() + + if path_info.startswith("/web/static"): + label = "assets" + elif path_info.startswith("/web/content"): + label = "filestore" + else: + label = "client" + + res = None + with REQUEST_TIME.labels(label).time(): + res = super(IrHttp, self)._dispatch() + + return res