9 azure backport (#353)

* fix: backport fix + backport modules to azure
This commit is contained in:
Vincent Renaville
2022-03-15 10:27:02 +01:00
committed by GitHub
co-authored by GitHub
parent 92bf8210ef
commit 61a7d392ff
19 changed files with 332 additions and 26 deletions
+5 -2
View File
@@ -27,10 +27,13 @@ virtualenv:
env:
matrix:
- LINT_CHECK="1"
- TESTS="1" ODOO_REPO="odoo/odoo" INCLUDE="cloud_platform_azure"
- TESTS="1" ODOO_REPO="OCA/OCB" INCLUDE="cloud_platform_azure"
- TESTS="1" ODOO_REPO="odoo/odoo" INCLUDE="cloud_platform_ovh"
- TESTS="1" ODOO_REPO="OCA/OCB" INCLUDE="cloud_platform_ovh"
- TESTS="1" ODOO_REPO="odoo/odoo" EXCLUDE="cloud_platform,cloud_platform_ovh"
- TESTS="1" ODOO_REPO="OCA/OCB" EXCLUDE="cloud_platform,cloud_platform_ovh"
- TESTS="1" ODOO_REPO="odoo/odoo" EXCLUDE="cloud_platform,cloud_platform_ovh,cloud_platform_azure"
- TESTS="1" ODOO_REPO="OCA/OCB" EXCLUDE="cloud_platform,cloud_platform_ovh,cloud_platform_azure"
global:
- VERSION="9.0" LINT_CHECK="0" TESTS="0"
+1 -1
View File
@@ -658,4 +658,4 @@ specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.
<http://www.gnu.org/licenses/>.
+37 -14
View File
@@ -129,25 +129,39 @@ class IrAttachment(models.Model):
return str.lower(storage_name)[:63]
@api.model
def _get_azure_container(self):
container_name = self._get_container_name()
blob_service_client = self._get_blob_service_client()
container_client = blob_service_client.get_container_client(container_name)
def _get_azure_container(self, container_name=None):
if not container_name:
container_name = self._get_container_name()
try:
# Create the container
container_client.create_container()
except ResourceExistsError:
pass
except HttpResponseError as error:
_logger.exception("Error during the creation of the Azure container")
raise exceptions.UserError(str(error))
blob_service_client = self._get_blob_service_client()
except exceptions.UserError:
_logger.exception(
"error accessing to storage '%s' please check credentials ",
container_name
)
return False
container_client = blob_service_client.get_container_client(container_name)
if not container_client.exists():
try:
# Create the container
container_client.create_container()
except HttpResponseError as error:
_logger.exception("Error during the creation of the Azure container")
raise exceptions.UserError(str(error))
return container_client
@api.model
def _store_file_read(self, fname, bin_size=False):
if fname.startswith("azure://"):
container_client = self._get_azure_container()
key = fname.replace("azure://", "", 1).lower()
if '/' in key:
container_name, key = key.split('/', 1)
else:
container_name = None
container_client = self._get_azure_container(container_name)
# if container cannot be retrived, abort reading from azure storage
if not container_client:
return ''
try:
blob_client = container_client.get_blob_client(key)
if bin_size:
@@ -166,11 +180,13 @@ class IrAttachment(models.Model):
location = self.env.context.get("storage_location") or self._storage()
if location == "azure":
container_client = self._get_azure_container()
if not container_client:
return ''
filename = "azure://%s/%s" % (container_client.container_name, key)
with io.BytesIO() as file:
blob_client = container_client.get_blob_client(key.lower())
file.write(bin_data)
file.seek(0)
filename = "azure://%s" % (key)
try:
blob_client.upload_blob(file, blob_type="BlockBlob")
except ResourceExistsError:
@@ -189,8 +205,15 @@ class IrAttachment(models.Model):
@api.model
def _store_file_delete(self, fname):
if fname.startswith("azure://"):
container_client = self._get_azure_container()
# if container cannot be retrived, abort reading from azure storage
key = fname.replace("azure://", "", 1).lower()
if '/' in key:
container_name, key = key.split('/', 1)
else:
container_name = None
container_client = self._get_azure_container(container_name)
if not container_client:
return ''
# delete the file only if it is on the current configured container
# otherwise, we might delete files used on a different environment
try:
@@ -9,9 +9,9 @@ from mock import patch
import keystoneauth1
from odoo.addons.base.tests.test_ir_attachment import TestIrAttachment
from odoo.addons.attachment_swift.models.ir_attachment import SwiftSessionStore
from odoo.addons.attachment_swift.swift_uri import SwiftUri
from openerp.addons.base.tests.test_ir_attachment import TestIrAttachment
from openerp.addons.attachment_swift.models.ir_attachment import SwiftSessionStore
from openerp.addons.attachment_swift.swift_uri import SwiftUri
class TestAttachmentSwift(TestIrAttachment):
@@ -6,7 +6,7 @@ import inspect
import logging
import os
import psycopg2
import odoo
import openerp
from contextlib import closing, contextmanager
from openerp import api, exceptions, fields, models, _
@@ -208,7 +208,7 @@ class IrAttachment(models.Model):
"""
with api.Environment.manage():
if new_cr:
registry = odoo.modules.registry.RegistryManager.get(
registry = openerp.modules.registry.RegistryManager.get(
self.env.cr.dbname
)
with closing(registry.cursor()) as cr:
+6
View File
@@ -0,0 +1,6 @@
Cloud Platform Azure
====================
Install addons specific to the Azure setup.
* The object storage is Azure blob storage
+2
View File
@@ -0,0 +1,2 @@
from __future__ import absolute_import
from . import models
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# 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": "9.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,
}
+2
View File
@@ -0,0 +1,2 @@
from __future__ import absolute_import
from . import cloud_platform
@@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
from __future__ import absolute_import
import re
import os
from openerp import models, api
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(models.AbstractModel):
_inherit = "cloud.platform"
@api.model
def _filestore_kinds(self):
kinds = super(CloudPlatform, self)._filestore_kinds()
kinds["azure"] = AZURE_STORE_KIND
return kinds
@api.model
def _platform_kinds(self):
kinds = super(CloudPlatform, self)._platform_kinds()
kinds.append("azure")
return kinds
@api.model
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
@api.model
def _check_filestore(self, environment_name):
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(ur"^[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()'."
)
@api.model
def install(self):
self._install("azure")
+4 -4
View File
@@ -11,7 +11,7 @@ from openerp.http import request as http_request
from openerp.tools.config import config
_logger = logging.getLogger('monitoring.http.requests')
_logger = logging.getLogger(u'monitoring.http.requests')
class IrHttp(models.AbstractModel):
@@ -28,8 +28,8 @@ class IrHttp(models.AbstractModel):
return response
def _monitoring_blacklist(self, request):
path_info = request.httprequest.environ.get('PATH_INFO')
if path_info.startswith('/longpolling/'):
path_info = request.httprequest.environ.get(u'PATH_INFO')
if path_info.startswith(u'/longpolling/'):
return True
return False
@@ -37,7 +37,7 @@ class IrHttp(models.AbstractModel):
return True
def _monitoring_info(self, request, response, begin, end):
path = request.httprequest.environ.get('PATH_INFO')
path = request.httprequest.environ.get(u'PATH_INFO')
info = {
# timing
'start_time': time.strftime("%Y-%m-%d %H:%M:%S",
+17
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
from __future__ import absolute_import
from . import controllers
from . import models
+23
View File
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
{
"name": "Monitoring: Prometheus Metrics",
"version": "9.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,
}
@@ -0,0 +1,2 @@
from __future__ import absolute_import
from . import prometheus_metrics
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
from __future__ import absolute_import
import logging
from openerp.http import Controller, route
_logger = logging.getLogger(__name__)
try:
from prometheus_client import generate_latest
except (ImportError, IOError), err:
_logger.warning(err)
class PrometheusController(Controller):
@route(u'/metrics', auth=u'public')
def metrics(self):
return generate_latest()
+2
View File
@@ -0,0 +1,2 @@
from __future__ import absolute_import
from . import ir_http
+50
View File
@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
from __future__ import with_statement
from __future__ import absolute_import
import logging
from openerp import models
from openerp.http import request
_logger = logging.getLogger(__name__)
try:
from prometheus_client import Summary, Counter
except (ImportError, IOError), err:
_logger.warning(err)
REQUEST_TIME = Summary(
"request_latency_sec", "Request response time in sec", ["query_type"]
)
LONGPOLLING_COUNT = Counter("longpolling", "Longpolling request count")
class IrHttp(models.AbstractModel):
_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, cls)._dispatch()
if path_info.startswith("/metrics"):
return super(IrHttp, cls)._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, cls)._dispatch()
return res
+1
View File
@@ -9,3 +9,4 @@ python-keystoneclient==3.22.0
keystoneauth1==3.14.0
# error with 5.x (ConstructorError: could not determine a constructor for the tag '!record')
PyYAML==4.2b4
prometheus_client==0.11.0