first commit

This commit is contained in:
Ruslan Grak 2025-01-07 10:00:02 +03:00
commit 626d8d3c56
349 changed files with 44175 additions and 0 deletions

5
.gitignore vendored Executable file
View File

@ -0,0 +1,5 @@
__pycache__
*/__pycache__
*/*/__pycache__
*.py[cod]
*$py.class

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Mohammad Awais
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

6
README.md Normal file
View File

@ -0,0 +1,6 @@
# odoo-saas-kubernetes
Production Ready Odoo SaaS based on Kubernetes
All documentation needs to be added.
if you have any questions contact us at https://cal.com/codetuple

View File

@ -0,0 +1,106 @@
======================
Verify email at signup
======================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github
:target: https://github.com/OCA/server-auth/tree/13.0/auth_signup_verify_email
:alt: OCA/server-auth
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/server-auth-13-0/server-auth-13-0-auth_signup_verify_email
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/251/13.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
This module extends the functionality of public sign up to force users to
provide a valid email address.
To achieve this, users are not required to provide a password at
sign up: they are asked for only at first login attempt.
**Table of contents**
.. contents::
:local:
Installation
============
* Install `email_validator <https://pypi.org/project/email-validator/>`_
with ``pip install email_validator`` or equivalent.
Configuration
=============
To configure this module, you need to:
* `Properly configure your outgoing email server(s)
<https://www.odoo.com/forum/help-1/question/how-to-configure-email-gateway-282#answer_290>`_.
* Go to *Settings > General Settings -> General settings*, search for
the *Users* section and enable *Free sign up* in *Customer account*.
Usage
=====
To use this module, you need to:
* Log out.
* `Sign up </web/signup>`_ with a valid email.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-auth/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/server-auth/issues/new?body=module:%20auth_signup_verify_email%0Aversion:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Antiun Ingeniería S.L.
* Tecnativa
Contributors
~~~~~~~~~~~~
* Rafael Blasco <rafaelbn@antiun.com>
* Jairo Llopis <yajo.sk8@gmail.com>
* Simone Orsi <simone.orsi@camptocamp.com>
* Alexandre Díaz <alexandre.diaz@tecnativa.com>
* Eugene Molotov <molotov@it-projects.info>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/server-auth <https://github.com/OCA/server-auth/tree/13.0/auth_signup_verify_email>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -0,0 +1,4 @@
# Copyright 2015 Antiun Ingeniería, S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import controllers

View File

@ -0,0 +1,17 @@
# Copyright 2015 Antiun Ingeniería, S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Verify email at signup",
"summary": "Force uninvited users to use a good email for signup",
"version": "14.0.1.0.0",
"category": "Authentication",
"website": "https://github.com/OCA/server-auth",
"author": "Antiun Ingeniería S.L., "
"Tecnativa, "
"Odoo Community Association (OCA)",
"license": "AGPL-3",
"depends": ["auth_signup"],
"external_dependencies": {"python": ["lxml", "email_validator"]},
"data": ["views/signup.xml"],
"installable": True,
}

View File

@ -0,0 +1,4 @@
# Copyright 2015 Antiun Ingeniería, S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import main

View File

@ -0,0 +1,78 @@
# Copyright 2015 Antiun Ingeniería, S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
from email_validator import EmailSyntaxError, EmailUndeliverableError, validate_email
from odoo import _
from odoo.http import request, route
from odoo.addons.auth_signup.controllers.main import AuthSignupHome
_logger = logging.getLogger(__name__)
class SignupVerifyEmail(AuthSignupHome):
@route()
def web_auth_signup(self, *args, **kw):
if request.params.get("login") and not request.params.get("password"):
return self.passwordless_signup()
return super().web_auth_signup(*args, **kw)
def passwordless_signup(self):
values = request.params
qcontext = self.get_auth_signup_qcontext()
# Check good format of e-mail
try:
validate_email(values.get("login", ""))
except EmailSyntaxError as error:
qcontext["error"] = getattr(
error, "message", _("That does not seem to be an email address."),
)
return request.render("auth_signup.signup", qcontext)
except EmailUndeliverableError as error:
qcontext["error"] = str(error)
return request.render("auth_signup.signup", qcontext)
except Exception as error:
qcontext["error"] = str(error)
return request.render("auth_signup.signup", qcontext)
if not values.get("email"):
values["email"] = values.get("login")
# preserve user lang
values["lang"] = request.context.get("lang", "")
# remove values that could raise "Invalid field '*' on model 'res.users'"
values.pop("redirect", "")
values.pop("token", "")
# Remove password
values["password"] = ""
sudo_users = request.env["res.users"].with_context(create_user=True).sudo()
try:
with request.cr.savepoint():
sudo_users.signup(values, qcontext.get("token"))
sudo_users.reset_password(values.get("login"))
except Exception as error:
# Duplicate key or wrong SMTP settings, probably
_logger.exception(error)
if (
request.env["res.users"]
.sudo()
.search([("login", "=", qcontext.get("login"))])
):
qcontext["error"] = _(
"Another user is already registered using this email" " address."
)
else:
# Agnostic message for security
qcontext["error"] = _(
"Something went wrong, please try again later or" " contact us."
)
return request.render("auth_signup.signup", qcontext)
qcontext["message"] = _("Check your email to activate your account!")
return request.render("auth_signup.reset_password", qcontext)

View File

@ -0,0 +1,42 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * auth_signup_verify_email
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 13.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2021-01-06 13:44+0000\n"
"Last-Translator: Rachid Al Assir <rachidalassir@gmail.com>\n"
"Language-Team: none\n"
"Language: ar\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n"
"X-Generator: Weblate 4.3.2\n"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Another user is already registered using this email address."
msgstr "هنالك مستخدم أخر مسجل بهذا البريد الإلكتروني."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Check your email to activate your account!"
msgstr "تحقق من بريدك الإلكتروني لتفعيل حسابك!"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Something went wrong, please try again later or contact us."
msgstr "حدث خطأ ما، يرجى المحاولة مرة أخرى لاحقًا أو الاتصال بنا."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "That does not seem to be an email address."
msgstr "لا يبدو أن هذا عنوان بريد إلكتروني."

View File

@ -0,0 +1,38 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * auth_signup_verify_email
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 13.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Another user is already registered using this email address."
msgstr ""
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Check your email to activate your account!"
msgstr ""
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Something went wrong, please try again later or contact us."
msgstr ""
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "That does not seem to be an email address."
msgstr ""

View File

@ -0,0 +1,45 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * auth_signup_verify_email
#
# Translators:
# Rudolf Schnapka <rs@techno-flex.de>, 2016
msgid ""
msgstr ""
"Project-Id-Version: server-tools (9.0)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-06-09 12:31+0000\n"
"PO-Revision-Date: 2016-05-31 14:47+0000\n"
"Last-Translator: OCA Transbot <transbot@odoo-community.org>\n"
"Language-Team: German (http://www.transifex.com/oca/OCA-server-tools-9-0/"
"language/de/)\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Another user is already registered using this email address."
msgstr ""
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Check your email to activate your account!"
msgstr "Prüfen Sie Ihre Email, um Ihr Konto zu aktivieren."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Something went wrong, please try again later or contact us."
msgstr ""
"Etwas ist schief gelaufen, bitte später nochmal versuchen oden an uns wenden."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "That does not seem to be an email address."
msgstr "Das schein keine Email-Adresse zu sein."

View File

@ -0,0 +1,44 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * auth_signup_verify_email
#
# Translators:
# Antonio Trueba, 2016
msgid ""
msgstr ""
"Project-Id-Version: server-tools (9.0)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-06-09 12:31+0000\n"
"PO-Revision-Date: 2016-05-31 14:47+0000\n"
"Last-Translator: OCA Transbot <transbot@odoo-community.org>\n"
"Language-Team: Spanish (http://www.transifex.com/oca/OCA-server-tools-9-0/"
"language/es/)\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Another user is already registered using this email address."
msgstr ""
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Check your email to activate your account!"
msgstr "Revise su correo para activar su cuenta."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Something went wrong, please try again later or contact us."
msgstr "Algo ha ido mal, por favor inténtelo de nuevo más tarde o contáctenos."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "That does not seem to be an email address."
msgstr "Eso no parece una dirección de email válida."

View File

@ -0,0 +1,44 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * auth_signup_verify_email
#
# Translators:
# Alexandre Papin <papin.alexandre@me.com>, 2017
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-03-18 02:19+0000\n"
"PO-Revision-Date: 2017-03-18 02:19+0000\n"
"Last-Translator: Alexandre Papin <papin.alexandre@me.com>, 2017\n"
"Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Another user is already registered using this email address."
msgstr ""
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Check your email to activate your account!"
msgstr "Un email vous à été envoyé pour activer votre compte."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Something went wrong, please try again later or contact us."
msgstr ""
"Une erreur est survenue, veuillez réessayer plus tard ou contactez nous."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "That does not seem to be an email address."
msgstr "Veuillez fournir une adresse email valide."

View File

@ -0,0 +1,45 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * auth_signup_verify_email
#
# Translators:
# Bole <bole@dajmi5.com>, 2017
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-10 00:47+0000\n"
"PO-Revision-Date: 2019-11-13 17:34+0000\n"
"Last-Translator: Bole <bole@dajmi5.com>\n"
"Language-Team: Croatian (https://www.transifex.com/oca/teams/23907/hr/)\n"
"Language: hr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 3.8\n"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Another user is already registered using this email address."
msgstr "Drugi korisnik je već registriran sa ovom mail adresom."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Check your email to activate your account!"
msgstr "Provjerite svoj e-mail za aktivaciu računa!"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Something went wrong, please try again later or contact us."
msgstr "Nešto nije u redu, molimo pokušajte ponovo ili nas kontaktirajte."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "That does not seem to be an email address."
msgstr "Ovo ne izgleda kao e-mail adresa."

View File

@ -0,0 +1,47 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * auth_signup_verify_email
#
# Translators:
# Bole <bole@dajmi5.com>, 2016
msgid ""
msgstr ""
"Project-Id-Version: server-tools (9.0)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-06-29 00:48+0000\n"
"PO-Revision-Date: 2016-06-13 08:38+0000\n"
"Last-Translator: Bole <bole@dajmi5.com>\n"
"Language-Team: Croatian (Croatia) (http://www.transifex.com/oca/OCA-server-"
"tools-9-0/language/hr_HR/)\n"
"Language: hr_HR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Another user is already registered using this email address."
msgstr ""
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Check your email to activate your account!"
msgstr "Provjerite svoj mail za aktiviranje računa"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Something went wrong, please try again later or contact us."
msgstr ""
"Nešto nije u redu, molimo pokušajte se prijaviti kasnije ili nas "
"kontaktirajte."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "That does not seem to be an email address."
msgstr "Ovo ne izgleda kao valjana mail adresa."

View File

@ -0,0 +1,46 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * auth_signup_verify_email
#
# Translators:
# Paolo Valier, 2016
msgid ""
msgstr ""
"Project-Id-Version: server-tools (9.0)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-06-09 12:31+0000\n"
"PO-Revision-Date: 2016-05-31 14:47+0000\n"
"Last-Translator: OCA Transbot <transbot@odoo-community.org>\n"
"Language-Team: Italian (http://www.transifex.com/oca/OCA-server-tools-9-0/"
"language/it/)\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Another user is already registered using this email address."
msgstr ""
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Check your email to activate your account!"
msgstr "Controlla la tua email per attivare il tuo account!"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Something went wrong, please try again later or contact us."
msgstr ""
"Qualcosa non è funzionato, prego provare più tardi altrimenti mettiti in "
"contatto con noi."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "That does not seem to be an email address."
msgstr "Questo non sembra essere un indirizzo email valido."

View File

@ -0,0 +1,47 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * auth_signup_verify_email
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: server-tools (8.0)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-11-27 14:01+0000\n"
"PO-Revision-Date: 2019-09-03 03:23+0000\n"
"Last-Translator: Rodrigo Macedo <rmsolucoeseminformatic4@gmail.com>\n"
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/oca/OCA-server-"
"tools-8-0/language/pt_BR/)\n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 3.8\n"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Another user is already registered using this email address."
msgstr "Outro usuário já está registrado usando este endereço de email."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Check your email to activate your account!"
msgstr "Verifique seu e-mail para ativar sua conta!"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Something went wrong, please try again later or contact us."
msgstr "Ocorreu um erro. Tente novamente mais tarde ou entre em contato."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "That does not seem to be an email address."
msgstr "Esse não parece ser um endereço de email."
#~ msgid "email"
#~ msgstr "email"

View File

@ -0,0 +1,45 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * auth_signup_verify_email
#
# Translators:
# Daniel Schweiger <danielcccasle@gmail.com>, 2017
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-22 01:11+0000\n"
"PO-Revision-Date: 2017-06-22 01:11+0000\n"
"Last-Translator: Daniel Schweiger <danielcccasle@gmail.com>, 2017\n"
"Language-Team: Romanian (https://www.transifex.com/oca/teams/23907/ro/)\n"
"Language: ro\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?"
"2:1));\n"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Another user is already registered using this email address."
msgstr ""
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Check your email to activate your account!"
msgstr "Ati primit un email pentru activarea acestui cont!"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Something went wrong, please try again later or contact us."
msgstr ""
"Sa produs o eroare, va rugam sa incercati mai tarziu sau contacteaza-ne."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "That does not seem to be an email address."
msgstr "Nu ati introdus o directie email corespunzatoare."

View File

@ -0,0 +1,46 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * auth_signup_verify_email
#
# Translators:
# Matjaž Mozetič <m.mozetic@matmoz.si>, 2015
msgid ""
msgstr ""
"Project-Id-Version: server-tools (9.0)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-06-09 12:31+0000\n"
"PO-Revision-Date: 2016-05-31 14:47+0000\n"
"Last-Translator: OCA Transbot <transbot@odoo-community.org>\n"
"Language-Team: Slovenian (http://www.transifex.com/oca/OCA-server-tools-9-0/"
"language/sl/)\n"
"Language: sl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n"
"%100==4 ? 2 : 3);\n"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Another user is already registered using this email address."
msgstr ""
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Check your email to activate your account!"
msgstr "Preverite svojo e-pošto za aktiviranje računa!"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Something went wrong, please try again later or contact us."
msgstr ""
"Nekaj je narobe. Ponovno poskusite kasneje ali pa stopite v stik z nami."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "That does not seem to be an email address."
msgstr "Kaže, da to ni e-poštni naslov."

View File

@ -0,0 +1,44 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * auth_signup_verify_email
#
# Translators:
# Ahmet Altınışık <aaltinisik@altinkaya.com.tr>, 2016
msgid ""
msgstr ""
"Project-Id-Version: server-tools (9.0)\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-06-09 12:31+0000\n"
"PO-Revision-Date: 2016-05-31 14:47+0000\n"
"Last-Translator: OCA Transbot <transbot@odoo-community.org>\n"
"Language-Team: Turkish (http://www.transifex.com/oca/OCA-server-tools-9-0/"
"language/tr/)\n"
"Language: tr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Another user is already registered using this email address."
msgstr ""
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Check your email to activate your account!"
msgstr "Hesabınızı aktive etmek için e-postanızı kontrol edin!"
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "Something went wrong, please try again later or contact us."
msgstr "Bişeyler ters gitti. Lütfen sonra tekrar deneyin ya da bize ulaşın."
#. module: auth_signup_verify_email
#: code:addons/auth_signup_verify_email/controllers/main.py:0
#, python-format
msgid "That does not seem to be an email address."
msgstr "Bu bir e-posta adresi gözükmüyor."

View File

@ -0,0 +1,6 @@
To configure this module, you need to:
* `Properly configure your outgoing email server(s)
<https://www.odoo.com/forum/help-1/question/how-to-configure-email-gateway-282#answer_290>`_.
* Go to *Settings > General Settings -> General settings*, search for
the *Users* section and enable *Free sign up* in *Customer account*.

View File

@ -0,0 +1,5 @@
* Rafael Blasco <rafaelbn@antiun.com>
* Jairo Llopis <yajo.sk8@gmail.com>
* Simone Orsi <simone.orsi@camptocamp.com>
* Alexandre Díaz <alexandre.diaz@tecnativa.com>
* Eugene Molotov <molotov@it-projects.info>

View File

@ -0,0 +1,5 @@
This module extends the functionality of public sign up to force users to
provide a valid email address.
To achieve this, users are not required to provide a password at
sign up: they are asked for only at first login attempt.

View File

@ -0,0 +1,2 @@
* Install `email_validator <https://pypi.org/project/email-validator/>`_
with ``pip install email_validator`` or equivalent.

View File

@ -0,0 +1,4 @@
To use this module, you need to:
* Log out.
* `Sign up </web/signup>`_ with a valid email.

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,454 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<title>Verify email at signup</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="verify-email-at-signup">
<h1 class="title">Verify email at signup</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/server-auth/tree/13.0/auth_signup_verify_email"><img alt="OCA/server-auth" src="https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/server-auth-13-0/server-auth-13-0-auth_signup_verify_email"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/251/13.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module extends the functionality of public sign up to force users to
provide a valid email address.</p>
<p>To achieve this, users are not required to provide a password at
sign up: they are asked for only at first login attempt.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#installation" id="id1">Installation</a></li>
<li><a class="reference internal" href="#configuration" id="id2">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="id3">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id4">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id5">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id6">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id7">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id8">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="installation">
<h1><a class="toc-backref" href="#id1">Installation</a></h1>
<ul class="simple">
<li>Install <a class="reference external" href="https://pypi.org/project/email-validator/">email_validator</a>
with <tt class="docutils literal">pip install email_validator</tt> or equivalent.</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#id2">Configuration</a></h1>
<p>To configure this module, you need to:</p>
<ul class="simple">
<li><a class="reference external" href="https://www.odoo.com/forum/help-1/question/how-to-configure-email-gateway-282#answer_290">Properly configure your outgoing email server(s)</a>.</li>
<li>Go to <em>Settings &gt; General Settings -&gt; General settings</em>, search for
the <em>Users</em> section and enable <em>Free sign up</em> in <em>Customer account</em>.</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id3">Usage</a></h1>
<p>To use this module, you need to:</p>
<ul class="simple">
<li>Log out.</li>
<li><a class="reference external" href="/web/signup">Sign up</a> with a valid email.</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id4">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/server-auth/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/server-auth/issues/new?body=module:%20auth_signup_verify_email%0Aversion:%2013.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id5">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id6">Authors</a></h2>
<ul class="simple">
<li>Antiun Ingeniería S.L.</li>
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id7">Contributors</a></h2>
<ul class="simple">
<li>Rafael Blasco &lt;<a class="reference external" href="mailto:rafaelbn&#64;antiun.com">rafaelbn&#64;antiun.com</a>&gt;</li>
<li>Jairo Llopis &lt;<a class="reference external" href="mailto:yajo.sk8&#64;gmail.com">yajo.sk8&#64;gmail.com</a>&gt;</li>
<li>Simone Orsi &lt;<a class="reference external" href="mailto:simone.orsi&#64;camptocamp.com">simone.orsi&#64;camptocamp.com</a>&gt;</li>
<li>Alexandre Díaz &lt;<a class="reference external" href="mailto:alexandre.diaz&#64;tecnativa.com">alexandre.diaz&#64;tecnativa.com</a>&gt;</li>
<li>Eugene Molotov &lt;<a class="reference external" href="mailto:molotov&#64;it-projects.info">molotov&#64;it-projects.info</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id8">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/server-auth/tree/13.0/auth_signup_verify_email">OCA/server-auth</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,4 @@
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_verify_email

View File

@ -0,0 +1,49 @@
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from lxml.html import document_fromstring
from mock import patch
from odoo.tests.common import HttpCase
from odoo.tools.misc import mute_logger
from odoo.addons.mail.models import mail_template
class UICase(HttpCase):
def setUp(self):
super().setUp()
if "website" in self.env:
# Enable public signup in website if it is installed; otherwise
# tests here would fail
current_website = self.env["website"].get_current_website()
current_website.auth_signup_uninvited = "b2c"
self.env["ir.config_parameter"].set_param("auth_signup.invitation_scope", "b2c")
self.data = {
"csrf_token": self.csrf_token(),
"name": "Somebody",
}
def html_doc(self, url="/web/signup", data=None, timeout=30):
"""Get an HTML LXML document."""
with patch(mail_template.__name__ + ".MailTemplate.send_mail"):
resp = self.url_open(url, data=data, timeout=timeout)
return document_fromstring(resp.content)
def csrf_token(self):
"""Get a valid CSRF token."""
doc = self.html_doc()
return doc.xpath("//input[@name='csrf_token']")[0].get("value")
def test_bad_email(self):
"""Test rejection of bad emails."""
self.data["login"] = "bad email"
doc = self.html_doc(data=self.data)
self.assertTrue(doc.xpath('//p[@class="alert alert-danger"]'))
@mute_logger("odoo.addons.auth_signup_verify_email.controllers.main")
def test_good_email(self):
"""Test acceptance of good emails."""
self.data["login"] = "good@example.com"
doc = self.html_doc(data=self.data)
self.assertTrue(doc.xpath('//p[@class="alert alert-success"]'))

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<template id="signup_fields" inherit_id="auth_signup.fields">
<xpath expr="//div[hasclass('field-password')]" position="attributes">
<attribute name="t-if">only_passwords</attribute>
</xpath>
<xpath expr="//div[hasclass('field-confirm_password')]" position="attributes">
<attribute name="t-if">only_passwords</attribute>
</xpath>
<xpath expr="//input[@name='login']" position="attributes">
<attribute name="type">email</attribute>
</xpath>
</template>
</odoo>

2
ct_client_backup/__init__.py Executable file
View File

@ -0,0 +1,2 @@
from . import models
from . import wizards

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
{
'name': "Clients Periodic Backups | SaaS",
'summary': """
Take Periodic Backups of Client Instances""",
'description': """
Take Periodic Backups of Client Instances""",
'author': "Muhammad Awais",
'website': "https://codetuple.io",
'category': 'Uncategorized',
'version': '2.0.0',
# any module necessary for this one to work correctly
'depends': ['base', 'kk_odoo_saas','queue_job'],
# always loaded
'data': [
# "security/security.xml",
'security/ir.model.access.csv',
'wizards/backup_restore.xml',
'views/views.xml',
'views/app_views.xml',
'data/backup_ignite_cron.xml',
],
"application": True,
}

View File

View File

@ -0,0 +1,14 @@
<?xml version="1.0"?>
<odoo>
<data>
<record id="backup_process_ignite_crone" model="ir.cron">
<field name="name">SaaS: Backup Process Ignite Cron</field>
<field name="model_id" ref="model_kk_odoo_saas_app"/>
<field name="state">code</field>
<field name="code">model.ignite_backup_server_cron()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,2 @@
from . import saas_app
from . import models

View File

@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
import os
from odoo import models, fields, api, _
from odoo.exceptions import UserError, MissingError
import requests
import xmlrpc
import logging
import base64
_logger = logging.getLogger(__name__)
class SaaSAppBackup(models.Model):
_name = 'kk_odoo_saas.app.backup'
_description = 'SaaS App Backup'
name = fields.Char()
app = fields.Many2one('kk_odoo_saas.app', 'SaaS App')
file_name = fields.Char(string="File Name")
file_path = fields.Char(string="File Path")
url = fields.Char(string="Url")
backup_date_time = fields.Datetime(string="Backup Time (UTC)")
status = fields.Selection(string="Status", selection=[('failed', 'Failed'), ('success', 'Success')])
message = fields.Char(string="Message")
file_size = fields.Char(string="File Size")
def download_db_file(self):
"""
to download the database backup, it stores the file in attachment
:return: Action
"""
file_path = self.file_path
_logger.info("------------ %r ----------------" % file_path)
if self.url:
return {
'type': 'ir.actions.act_url',
'url': self.url,
'target': 'new',
}
try:
with open(file_path, 'rb') as reader:
result = base64.b64encode(reader.read())
except IOError as e:
raise MissingError('Unable to find File on the path')
attachment_obj = self.env['ir.attachment'].sudo()
name = self.file_name
attachment_id = attachment_obj.create({
'name': name,
'datas': result,
'public': False
})
download_url = '/web/content/' + str(attachment_id.id) + '?download=true'
_logger.info("--- %r ----" % download_url)
self.url = download_url
return {
'type': 'ir.actions.act_url',
'url': download_url,
'target': 'new',
}
def action_restore_backup_to_instance(self, restore_to_id=False):
"""
it will restore the backup to a new instance,
the instance should be created manually,
and there should be no database at new_instance.com/web/database/selector
:param: restore_to_id is kk_saa_app object on which we have to restore backup
:return: False
"""
if self.app and restore_to_id:
restore_url = restore_to_id.get_url()
if self.file_path and os.path.exists(self.file_path) and requests.get(restore_url).status_code < 400:
db_list = []
try:
db_list = xmlrpc.client.ServerProxy(restore_url + '/xmlrpc/db').list()
except xmlrpc.client.ProtocolError as e:
_logger.info("There is no database on Db selector")
_logger.info("All Databases on Postgres Server -> {} <-".format(db_list))
_logger.info("New Db name: {}".format(restore_to_id.app_name))
if restore_to_id.app_name not in db_list:
self.restore_backup_to_client(self.file_path, restore_url, restore_to_id.app_name,
restore_to_id.backup_master_pass)
else:
raise UserError("Cant restore Backup, Database already existed, please delete it.")
else:
raise UserError("Cant restore Backup! the url is not accessible or backup file not exists.")
else:
_logger.error("Cant restore Backup, Backup Id, or Restore App Missing")
raise UserError("Cant restore Backup, Backup Id, or Restore App Missing")
def restore_backup_to_client(self, file_path, restore_url, db_name, master_pwd):
if file_path and restore_url and db_name and master_pwd:
restore_url = restore_url + '/web/database/restore'
data = {
'master_pwd': master_pwd,
'name': db_name,
'copy': 'true',
'backup_file': '@' + file_path
}
backup = open(file_path, "rb")
try:
response = requests.post(restore_url, data=data, files={"backup_file": backup})
if response.status_code == 200:
_logger.info("Restore Done, this is the response Code: {}".format(response.status_code))
else:
_logger.info("Restore Done, this is the response Code: {}".format(response.status_code))
return {
'success': True,
}
except Exception as e:
return {
'success': False,
}
else:
_logger.error("Cant restore Db One of the parameter is Missing")
@api.model
def create(self, vals):
vals['name'] = self.env['ir.sequence'].next_by_code('saas_app.backup')
res = super(SaaSAppBackup, self).create(vals)
return res
def calc_backup_size(self):
if not os.path.exists(self.file_path):
return
# calculate file size in KB, MB, GB
def convert_bytes(size):
""" Convert bytes to KB, or MB or GB"""
for x in ['bytes', 'KB', 'MB', 'GB', 'TB']:
if size < 1024.0:
return "%3.1f %s" % (size, x)
size /= 1024.0
self.file_size = convert_bytes(os.path.getsize(self.file_path))

View File

@ -0,0 +1,124 @@
from datetime import timedelta
import requests
from odoo import models, fields, api, _
import logging
import os
_logger = logging.getLogger(__name__)
class SaaSApp(models.Model):
_inherit = 'kk_odoo_saas.app'
backup_db_name = fields.Char(string="Database Name", default=lambda a: a.client_db_name)
backup_master_pass = fields.Char(string="Master Password")
backups_enabled = fields.Boolean()
backups = fields.Many2many(comodel_name='kk_odoo_saas.app.backup', string='Backups')
def action_create_backup(self):
"""
It is being called from 2 locations
:return:
"""
for app in self:
response = self.backup_db()
backup = self.env['kk_odoo_saas.app.backup'].create({'backup_date_time': fields.Datetime.now(),
'app': app.id,
'file_name': response.get('filename'),
'file_path': response.get('filepath'),
'message': response.get('message')
})
if response.get('success'):
backup.write({'status': 'success'})
else:
backup.write({'status': 'failed'})
app.write({'backups': [(4, backup.id)]})
def action_delete_old_backup(self):
for app in self:
for backup in app.backups:
if backup.backup_date_time < fields.Datetime.now() - timedelta(days=7.0):
if os.path.exists(backup.file_path):
try:
os.remove(backup.file_path)
if backup.url:
# deleting the attachments related to this backup
att_id = backup.url.replace('?download=true', '').replace('/web/content/', '')
if att_id:
try:
attch_id = int(att_id)
if attch_id:
self.env['ir.attachment'].browse([attch_id]).unlink()
except ValueError as e:
_logger.error(e)
backup.unlink()
except OSError as e:
_logger.error("Error while deleting file: %s - %s." % (e.filename, e.strerror))
else:
_logger.error("The file does not exist")
def backup_db(self):
"""
Actual Backup function
:return:
"""
# get the creds for db manager
data = {
'master_pwd': self.backup_master_pass,
'name': self.backup_db_name,
'backup_format': 'zip'
}
client_url = 'https://{0}{1}'.format(self.sub_domain_name, self.domain_name)
msg = ''
# where we want to store backups, in the linux user, with which the odoo-service is running
backup_dir = os.path.join(os.path.expanduser('~'), 'client_backups')
if not os.path.exists(backup_dir):
os.mkdir(backup_dir)
backup_dir = os.path.join(backup_dir, self.sub_domain_name)
if not os.path.exists(backup_dir):
os.mkdir(backup_dir)
client_url += '/web/database/backup'
# Without Streaming method
# response = requests.post(client_url, data=data)
# Streaming zip, so that everything is not stored in RAM.
try:
filename = self.backup_db_name + '-' + fields.Datetime.now().strftime("%m-%d-%Y-%H-%M") + '.zip'
backed_up_file_path = os.path.join(backup_dir, filename)
with requests.post(client_url, data=data, stream=True) as response:
response.raise_for_status()
with open(os.path.join(backup_dir, filename), 'wb') as file:
for chunk in response.iter_content(chunk_size=1024):
if chunk:
file.write(chunk)
msg = 'Database backup Successful at ' + fields.Datetime.now().strftime("%m-%d-%Y-%H:%M:%S")
return {
'success': True,
'msg': msg,
'filename': filename,
'filepath': backed_up_file_path
}
except Exception as e:
msg = 'Failed at ' + fields.Datetime.now().strftime("%m-%d-%Y-%H:%M:%S") + ' ' + str(e)
return {
'success': False,
'msg': msg
}
@api.model
def ignite_backup_server_cron(self):
"""
A Scheduled Action which will take new backups and del old
:return: False
"""
# search for saas instance in lanched and modified states and backups enabled
apps = self.env['kk_odoo_saas.app'].sudo().search(
[('status', 'in', ['l', 'm']), ('backups_enabled', '=', True)])
for app in apps:
app.action_create_backup()
app.action_delete_old_backup()

View File

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_saas_app_backups,access_saas_app_backups,model_kk_odoo_saas_app_backup,kk_odoo_saas.group_saas_manager,1,1,1,1
access_saas_app_backup_restore_wizard,SaaS Manager Backup Restore Permissions,model_saas_client_backup_restore_wizard,kk_odoo_saas.group_saas_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_saas_app_backups access_saas_app_backups model_kk_odoo_saas_app_backup kk_odoo_saas.group_saas_manager 1 1 1 1
3 access_saas_app_backup_restore_wizard SaaS Manager Backup Restore Permissions model_saas_client_backup_restore_wizard kk_odoo_saas.group_saas_manager 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Inherit Form View to Modify it -->
<record id="saas_app_form_backup_inherit" model="ir.ui.view">
<field name="name">SaaS App inherit for Backups</field>
<field name="model">kk_odoo_saas.app</field>
<field name="inherit_id" ref="kk_odoo_saas.kk_odoo_saas_app_view_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='k8s_logs']" position="after">
<page name="backups" string="Backups">
<group>
<group>
<field name="backups_enabled" widget="boolean_toggle"/>
<field name="backup_db_name"
attrs="{'required': [('backups_enabled', '=', True)], 'invisible': [('backups_enabled', '!=', True)]}"/>
</group>
<group>
<field name="backup_master_pass"
attrs="{'required': [('backups_enabled', '=', True)], 'invisible': [('backups_enabled', '!=', True)]}"
password="1"/>
<button name="action_create_backup"
type="object"
string="Take Backup"
attrs="{'invisible': ['|', ('status','in',('d','del')),('backups_enabled', '=', False)]}"
/>
</group>
</group>
<group>
<field name="backups" nolabel="1" readonly="1"/>
</group>
</page>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="kk_odoo_saas_app_backup_form_view" model="ir.ui.view">
<field name="name">kk_odoo_saas.kk_odoo_saas.app.backup.form</field>
<field name="model">kk_odoo_saas.app.backup</field>
<field name="arch" type="xml">
<form>
<header>
<button name="download_db_file"
type="object"
string="Download File"
/>
<button name="action_restore_backup_to_instance"
type="object"
string="Restore to Instance"
/>
</header>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1>
<field name="name" placeholder="Title" readonly="1"/>
</h1>
</div>
<group>
<group>
<field name="app"/>
<field name="file_name"/>
<field name="file_path"/>
</group>
<group>
<field name="url"/>
<field name="backup_date_time"/>
<field name="status"/>
<field name="message"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="kk_odoo_saas_app_backup_tree_view" model="ir.ui.view">
<field name="name">kk_odoo_saas.kk_odoo_saas.app.backup.tree</field>
<field name="model">kk_odoo_saas.app.backup</field>
<field name="arch" type="xml">
<tree string="App Backup" decoration-success="status=='success'" decoration-danger="status!='success'">
<field name="name" readonly="1"/>
<field name="file_name"/>
<field name="backup_date_time"/>
<field name="status"/>
<field name="file_size"/>
<button name="download_db_file"
type="object" icon="fa-cloud-download"
string="Download Zip File"
class="btn-secondary"
/>
<button name="ct_client_backup.action_saas_client_backup_restore_wizard"
string="Restore to Instance" type="action" icon="fa-cloud-upload"
class="btn-secondary"
context="{'default_backup_id':id}"
attrs="{'invisible':[('status','=', 'failed')]}" />
<button name="calc_backup_size" type="object"
icon="fa-refresh" class="btn-secondary"
string="Calculate file Size"/>
</tree>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1 @@
from . import backup_restore

View File

@ -0,0 +1,23 @@
from odoo import fields, models
import logging
from odoo.exceptions import UserError, MissingError
_logger = logging.getLogger(__name__)
class BackupRestore(models.TransientModel):
_name = 'saas.client.backup.restore.wizard'
name = fields.Char('Name')
backup_id = fields.Many2one('kk_odoo_saas.app.backup', 'Backup Name')
restore_to = fields.Many2one('kk_odoo_saas.app', 'Restore Backup To')
def action_call_restore_function(self):
"""
It will call the Backup Function Async, Thanks to queue_job module
:return:
"""
if self.backup_id and self.backup_id.app and self.restore_to:
self.backup_id.action_restore_backup_to_instance(self.restore_to)
else:
_logger.error("Cant restore Backup, Backup Id, or Restore App Missing")
raise UserError("Cant restore Backup, Backup Id, or Restore App Missing")

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="saas_client_backup_restore_view_form" model="ir.ui.view">
<field name="name">saas_client_backup_restore_view_form</field>
<field name="model">saas.client.backup.restore.wizard</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="name"/>
<field name="backup_id" required="1"/>
</group>
<group>
<field name="restore_to" string="Restore to SaaS Instance" required="1"/>
</group>
</group>
</sheet>
<footer>
<button
name="action_call_restore_function"
string="Start Restoring Process"
type="object"
class="oe_highlight"
/>
<button string="Cancel" class="oe_link" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_saas_client_backup_restore_wizard" model="ir.actions.act_window">
<field name="name">Restore the Backup to SaaS App</field>
<field name="res_model">saas.client.backup.restore.wizard</field>
<field name="view_mode">form</field>
<field name="view_id" ref="saas_client_backup_restore_view_form"/>
<field name="target">new</field>
<field name="binding_model_id" ref="ct_client_backup.model_kk_odoo_saas_app_backup"/>
</record>
</data>
</odoo>

25
info.yml Normal file
View File

@ -0,0 +1,25 @@
# Dynamic pv class
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp2
namespace: default
annotations:
storageclass.beta.kubernetes.io/is-default-class: "true"
labels:
addonmanager.kubernetes.io/mode: Reconcile
provisioner: k8s.io/minikube-hostpath
reclaimPolicy: Retain
---
# Claim for dynamic PV
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: claim1
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

6
kk_odoo_saas/__init__.py Executable file
View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models
from . import utils
from . import wizards

50
kk_odoo_saas/__manifest__.py Executable file
View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
{
'name': "CT Odoo SaaS",
'summary': """
Out of the Box SaaS Module based on Kubernetes""",
'description': """
Out of the Box SaaS Module based on Kubernetes
""",
'author': "Muhammad Awais",
'website': "https://codetuple.io",
'category': 'Uncategorized',
'version': '2.0.0',
# any module necessary for this one to work correctly
'depends': ['base', 'smile_log', 'website_sale', 'product',
'portal', 'auth_signup_verify_email', 'sale_subscription',
'queue_job'],
'external_dependencies': {
'python': [
'kubernetes',
],
},
# always loaded
'data': [
"security/security.xml",
'security/ir.model.access.csv',
'wizards/saas_app_delete.xml',
'wizards/update_docker_image.xml',
'views/app_views.xml',
'views/config_views.xml',
'views/assets.xml',
'views/saas_app_website.xml',
'views/templates.xml',
'views/sale_subscription.xml',
'views/logs_viewer.xml',
'views/res_config_settings_views.xml',
'views/saas_package_views.xml',
'data/data.xml',
'data/email_templates.xml',
],
'qweb': [
'static/src/xml/base.xml',
],
"application": True,
}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import logs_viewer

View File

@ -0,0 +1,17 @@
from odoo.http import route, request, Controller
import logging
from odoo.addons.portal.controllers.portal import CustomerPortal
_logger = logging.getLogger(__name__)
class SaaSAppsLogViewer(CustomerPortal):
@route("/saas/instance/<int:app_id>", type="http", auth="user", methods=['GET'], website=True)
def saas_app_log_viewer(self, app_id, **values):
saas_app = request.env["kk_odoo_saas.app"].sudo().browse(app_id)
if request.params.get('_'):
logs = saas_app.get_timed_pod_logs(since_seconds=5)
return logs
return request.render(
"kk_odoo_saas.saas_app_log_viewer", values
)

103
kk_odoo_saas/data/data.xml Executable file
View File

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_attribute_subscription" model="product.attribute">
<field name="name">Subscription (SaaS)</field>
<field name="sequence">30</field>
<field name="display_type">select</field>
</record>
<record id="product_attribute_value_subscription_annually" model="product.attribute.value">
<field name="name">Annually</field>
<field name="attribute_id" ref="product_attribute_subscription"/>
<field name="sequence">10</field>
</record>
<record id="product_attribute_value_subscription_monthly" model="product.attribute.value">
<field name="name">Monthly</field>
<field name="attribute_id" ref="product_attribute_subscription"/>
<field name="sequence">20</field>
</record>
<record id="product_attribute_value_subscription_trial" model="product.attribute.value">
<field name="name">Trial</field>
<field name="attribute_id" ref="product_attribute_subscription"/>
<field name="sequence">30</field>
</record>
<record id="product_users" model="product.template">
<field name="name">Users</field>
<field name="sale_ok" eval="True" />
<field name="purchase_ok" eval="False" />
<field name="is_saas_product" eval="True" />
<field name="type">service</field>
<field name="recurring_invoice" eval="True"/>
<field name="list_price">0</field>
</record>
<record id="product_users_attribute_subscription_line" model="product.template.attribute.line">
<field name="product_tmpl_id" ref="product_users" />
<field name="attribute_id" ref="product_attribute_subscription" />
<field name="value_ids" eval="[(6, 0, [
ref('kk_odoo_saas.product_attribute_value_subscription_annually'),
ref('product_attribute_value_subscription_monthly'),
ref('product_attribute_value_subscription_trial'),
])]" />
</record>
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'kk_odoo_saas.product_users_attribute_subscription_value_annually',
'record': obj().env.ref('kk_odoo_saas.product_users_attribute_subscription_line').product_template_value_ids[0],
'noupdate': True,
}, {
'xml_id': 'kk_odoo_saas.product_users_attribute_subscription_value_monthly',
'record': obj().env.ref('kk_odoo_saas.product_users_attribute_subscription_line').product_template_value_ids[1],
'noupdate': True,
}, {
'xml_id': 'kk_odoo_saas.product_users_attribute_subscription_value_trial',
'record': obj().env.ref('kk_odoo_saas.product_users_attribute_subscription_line').product_template_value_ids[2],
'noupdate': True,
}]"/>
</function>
<record id="product_users_attribute_subscription_value_annually" model="product.template.attribute.value">
<field name="price_extra">120</field>
</record>
<record id="product_users_attribute_subscription_value_monthly" model="product.template.attribute.value">
<field name="price_extra">12.5</field>
</record>
<record id="product_users_attribute_subscription_value_trial" model="product.template.attribute.value">
<field name="price_extra">0</field>
</record>
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'kk_odoo_saas.product_users_monthly',
'record': obj().env.ref('kk_odoo_saas.product_users')._get_variant_for_combination(obj().env.ref('kk_odoo_saas.product_users_attribute_subscription_value_monthly')),
'noupdate': True,
}, {
'xml_id': 'kk_odoo_saas.product_users_annually',
'record': obj().env.ref('kk_odoo_saas.product_users')._get_variant_for_combination(obj().env.ref('kk_odoo_saas.product_users_attribute_subscription_value_annually')),
'noupdate': True,
}, {
'xml_id': 'kk_odoo_saas.product_users_trial',
'record': obj().env.ref('kk_odoo_saas.product_users')._get_variant_for_combination(obj().env.ref('kk_odoo_saas.product_users_attribute_subscription_value_trial')),
'noupdate': True,
},]"/>
</function>
<data noupdate='1'>
<record id="app_backup_sequence" model="ir.sequence">
<field name="name">Backup Name</field>
<field name="code">saas_app.backup</field>
<field name="prefix">BACKUP</field>
<field name="padding">6</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,143 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Email template for new users app -->
<record id="app_invitation_email" model="mail.template">
<field name="name">KK SaaS: App Invitation</field>
<field name="model_id" ref="kk_odoo_saas.model_kk_odoo_saas_app"/>
<field name="subject">Here are the Credentials of your Instance</field>
<field name="email_from">"${object.admin_user.company_id.name | safe}" &lt;${(object.admin_user.company_id.email ) | safe}&gt;</field>
<field name="email_to">${object.admin_user.email_formatted | safe}</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0"
style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;">
<tr>
<td align="center">
<table border="0" cellpadding="0" cellspacing="0" width="590"
style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590"
style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr>
<td valign="middle">
<span style="font-size: 10px;">Welcome to SaaS</span>
<br/>
<span style="font-size: 20px; font-weight: bold;">
${object.admin_user.name}
</span>
</td>
<td valign="middle" align="right">
<img src="/logo.png?company=${object.admin_user.company_id.id}"
style="padding: 0px; margin: 0px; height: auto; width: 80px;"
alt="${object.admin_user.company_id.name}"/>
</td>
</tr>
<tr>
<td colspan="2" style="text-align:center;">
<hr width="100%"
style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td>
</tr>
</table>
</td>
</tr>
<!-- CONTENT -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590"
style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr>
<td valign="top" style="font-size: 13px;">
<div>
Dear ${object.admin_user.name},
<br/>
<br/>
You have been invited by ${object.create_uid.name} of
${object.admin_user.company_id.name} to connect on SaaS App.
% set website_url = object.get_url()
<br/>
Your SaaS domain is:
<b>
<a href='${website_url}'>${website_url}</a>
</b>
<br/>
Your sign in email is:
<b>
<a href="${website_url}/web/login?login=${object.admin_user.email}"
target="_blank">${object.admin_user.email}
</a>
</b>
<br/>
Your Password is:
<b>
<p>${object.login_pwd}</p>
</b>
<br/>
<br/>
<br/>
<br/>
Enjoy SaaS!
<br/>
--<br/>The ${object.admin_user.company_id.name} Team
</div>
</td>
</tr>
<tr>
<td style="text-align:center;">
<hr width="100%"
style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
</td>
</tr>
</table>
</td>
</tr>
<!-- FOOTER -->
<tr>
<td align="center" style="min-width: 590px;">
<table border="0" cellpadding="0" cellspacing="0" width="590"
style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
<tr>
<td valign="middle" align="left">
${object.admin_user.company_id.name}
</td>
</tr>
<tr>
<td valign="middle" align="left" style="opacity: 0.7;">
${object.admin_user.company_id.phone}
% if object.admin_user.company_id.email
|
<a href="'mailto:%s' % ${object.admin_user.company_id.email}"
style="text-decoration:none; color: #454748;">
${object.admin_user.company_id.email}
</a>
% endif
% if object.admin_user.company_id.website
|
<a href="'%s' % ${object.admin_user.company_id.website}"
style="text-decoration:none; color: #454748;">
${object.admin_user.company_id.website}
</a>
% endif
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</field>
<field name="lang">${object.admin_user.lang}</field>
<field name="auto_delete" eval="True"/>
</record>
</data>
</odoo>

14
kk_odoo_saas/models/__init__.py Executable file
View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from . import api
from . import models
from . import k8s_config
from . import saas_period_product_mixin
from . import product_template
from . import saas_app_website
from . import sale_subscription
from . import sale_order
from . import res_config_settings
from . import saas_package

514
kk_odoo_saas/models/api.py Normal file
View File

@ -0,0 +1,514 @@
# -*- coding: utf-8 -*-
from kubernetes import config, client
from kubernetes.stream import stream
import yaml
import psycopg2
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.addons.smile_log.tools import SmileDBLogger
class KubernetesApi(models.AbstractModel):
_name = 'kk_odoo_saas.kubernetes.api'
# Deployment =======================================================================================================
def deploy_api_main(self, namespace='default'):
self.ensure_one()
# Apply config
try:
cluster_config = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(cluster_config)
except config.config_exception.ConfigException as e:
self._logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
# Deploy
self.deploy_api_pv_claim(namespace=namespace)
self.deploy_api_deployment(namespace=namespace)
self.deploy_api_service(namespace=namespace)
self.deploy_api_ingress_http(namespace=namespace)
def deploy_api_pv_claim(self, namespace='default'):
specs = client.V1PersistentVolumeClaimSpec(
access_modes=['ReadWriteOnce'],
storage_class_name="gp2",
resources=client.V1ResourceRequirements(
requests={'storage': '1Gi'}
)
)
meta_data = client.V1ObjectMeta(
name=self._name_pv_claim,
labels={'app': self.app_name}
)
# ============================================================================
dep = client.V1PersistentVolumeClaim(
api_version='v1',
kind='PersistentVolumeClaim',
metadata=meta_data,
spec=specs
)
try:
resp = self.core_v1_api.create_namespaced_persistent_volume_claim(body=dep, namespace=namespace)
self._logger.info(f'Volume created. status={resp.metadata.name}')
except client.exceptions.ApiException as e:
self._logger.error(msg=str(e))
def deploy_api_deployment(self, namespace='default'):
res_limits = {'ephemeral-storage': '1Gi'}
image_pull_secrets = []
if self.docker_image.is_pvt_dkr_repo and self.docker_image.b64_dkr_config:
if self.deploy_api_repo_secret(namespace='default'):
sec_name = f'{self.app_name}-dkr-registry-key'
image_pull_secrets.append(client.V1LocalObjectReference(name=sec_name))
meta_data = client.V1ObjectMeta(name=self._name_deployment, labels={'app': self.app_name})
# ODOO ARGS ++++++++++++++++++++++++++++++++++++++++++++++++
args_odoo = [f'--database={self.sub_domain_name}']
if self.demo_data:
args_odoo.append('--without-demo=False')
else:
args_odoo.append('--without-demo=True')
if self.module_ids:
module_names = ','.join(self.module_ids.mapped('name'))
args_odoo.append(f'--init={module_names}')
# ODOO ARGS ++++++++++++++++++++++++++++++++++++++++++++++++
limits = client.V1ResourceRequirements(limits=res_limits)
node_selector = {}
if self and self.is_dedicated_node and self.node_id:
node_selector['kubernetes.io/hostname'] = self.node_id.name
odoo_container = client.V1Container(
name='odoo',
image=self._name_image,
env=[
client.V1EnvVar(name="HOST", value=self.db_server_id.server_url),
client.V1EnvVar(name="USER", value=self.db_server_id.master_username),
client.V1EnvVar(name="PASSWORD", value=self.db_server_id.master_pass),
client.V1EnvVar(name="PORT", value=self.db_server_id.server_port),
client.V1EnvVar(name="ODOO_HTTP_SOCKET_TIMEOUT", value="100"),
],
ports=[client.V1ContainerPort(container_port=8069, name="odoo-port")],
args=args_odoo,
image_pull_policy='Always',
resources=limits,
volume_mounts=[
client.V1VolumeMount(name=f'{self.app_name}-odoo-web-pv-storage', mount_path='/var/lib/odoo/')
]
)
volume_claim = client.V1PersistentVolumeClaimVolumeSource(claim_name=f'{self.app_name}-odoo-web-pv-claim')
volume = client.V1Volume(name=f'{self.app_name}-odoo-web-pv-storage', persistent_volume_claim=volume_claim)
strategy = client.V1DeploymentStrategy(type='Recreate')
spec = client.V1PodSpec(
containers=[odoo_container],
volumes=[volume],
image_pull_secrets=image_pull_secrets,
security_context=client.V1PodSecurityContext(
run_as_group=101,
run_as_user=101,
fs_group=101,
fs_group_change_policy='Always'
),
node_selector=node_selector,
)
template = client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(labels={'app': self.app_name, 'tier': "backend"}),
spec=spec
)
selector = client.V1LabelSelector(match_labels={'app': self.app_name, 'tier': 'backend'})
# Spec
specs = client.V1DeploymentSpec(replicas=1, strategy=strategy, selector=selector, template=template)
# =================================================
deployment = client.V1Deployment(api_version="apps/v1", kind="Deployment", metadata=meta_data, spec=specs)
k8s_apps_v1 = client.AppsV1Api()
try:
resp = k8s_apps_v1.create_namespaced_deployment(body=deployment, namespace=namespace)
self._logger.info("Deployment created. name='%s'" % resp.metadata.name)
except client.exceptions.ApiException as e:
self._logger.error(str(e))
def deploy_api_service(self, namespace='default'):
specs = client.V1ServiceSpec(
selector={'app': self.app_name, 'tier': 'backend'},
ports=[client.V1ServicePort(name='odoo-port', protocol='TCP', port=8069, target_port=8069)],
type='NodePort'
)
metadata = client.V1ObjectMeta(name=self._name_service, labels={'app': self.app_name})
# ============================================
body = client.V1Service(api_version='v1', kind='Service', metadata=metadata, spec=specs)
try:
service = self.core_v1_api.create_namespaced_service(namespace=namespace, body=body)
self._logger.info(f'Service created. status={service.metadata.name}')
except client.exceptions.ApiException as e:
self._logger.error(str(e))
def deploy_api_ingress_http(self, namespace='default'):
odoo_urls = [self._name_host]
# === Main rule ===
rules = [
client.V1IngressRule(
host=self._name_host,
http=client.V1HTTPIngressRuleValue(
paths=[
client.V1HTTPIngressPath(
path='/',
path_type='Prefix', # ImplementationSpecific
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(number=8069), name=self._name_service,
)
)
),
]
)
)
]
# === Additional rules ===
if self and self.custom_domain_ids:
for custom_domain in self.custom_domain_ids:
rules.append(
client.V1IngressRule(
host=custom_domain.name,
http=client.V1HTTPIngressRuleValue(
paths=[
client.V1HTTPIngressPath(
path='/',
path_type='Prefix',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(number=80), name=self._name_service,
)
)
),
]
)
)
)
odoo_urls.append(custom_domain.name)
body = client.V1Ingress(
kind='Ingress',
metadata=client.V1ObjectMeta(
name=self._name_ingress,
labels={'app': self.app_name},
annotations={
'kubernetes.io/ingress.class': 'nginx',
'cert-manager.io/cluster-issuer': 'letsencrypt-prod',
},
),
spec=client.V1IngressSpec(rules=rules)
)
try:
self.networking_v1_api.create_namespaced_ingress(namespace=namespace, body=body)
except client.exceptions.ApiException as e:
self._logger.error(str(e))
def deploy_api_ingress_https(self, namespace='default'):
odoo_urls = [self._name_host]
# === Main rule ===
rules = [
client.V1IngressRule(
host=self._name_host,
http=client.V1HTTPIngressRuleValue(
paths=[
client.V1HTTPIngressPath(
path='/',
path_type='Prefix', # ImplementationSpecific
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(number=8069), name=self._name_service,
)
)
),
]
)
)
]
# === Additional rules ===
if self and self.custom_domain_ids:
for custom_domain in self.custom_domain_ids:
rules.append(
client.V1IngressRule(
host=custom_domain.name,
http=client.V1HTTPIngressRuleValue(
paths=[
client.V1HTTPIngressPath(
path='/',
path_type='Prefix',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(number=80), name=self._name_service,
)
)
),
]
)
)
)
odoo_urls.append(custom_domain.name)
body = client.V1Ingress(
kind='Ingress',
metadata=client.V1ObjectMeta(
name=self._name_ingress,
labels={'app': self.app_name},
annotations={
'kubernetes.io/ingress.class': 'nginx',
'cert-manager.io/cluster-issuer': 'cert-manager', # 'letsencrypt-prod',
},
),
spec=client.V1IngressSpec(
rules=rules,
tls=[client.V1IngressTLS(hosts=odoo_urls, secret_name=self._name_secret)])
)
try:
self.networking_v1_api.create_namespaced_ingress(namespace=namespace, body=body)
except client.exceptions.ApiException as e:
self._logger.error(str(e))
def deploy_api_repo_secret(self, namespace="default"):
secret = client.V1Secret(
metadata=client.V1ObjectMeta(
name=self._name_secret,
labels={
"app": self.app_name,
"tier": "backend"
}
),
data={
'.dockerconfigjson': self.docker_image.b64_dkr_config
},
type='kubernetes.io/dockerconfigjson',
)
try:
resp = self.core_v1_api.create_namespaced_secret(body=secret, namespace=namespace)
self._logger.info("Secret created. name='%s'" % resp.metadata.name)
return True
except client.exceptions.ApiException as e:
self._logger.error(str(e))
return False
# Remove ===========================================================================================================
def remove_api_main(self, delete_db, delete_pv, delete_svc, delete_ing, delete_deployment, namespace='default'):
try:
cluster_config = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(cluster_config)
except config.config_exception.ConfigException as e:
self._logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
if delete_pv:
self.remove_api_pv_claim(namespace=namespace)
if delete_svc:
self.remove_api_service(namespace=namespace)
if delete_deployment:
self.remove_api_deployment(namespace=namespace)
if delete_ing:
self.remove_api_ingress(namespace=namespace)
if delete_db:
self.remove_api_db()
self.delete_job_task()
def remove_api_deployment(self, namespace):
try:
deployment = self.apps_v1_api.delete_namespaced_deployment(name=self._name_deployment, namespace=namespace)
if self.is_custom_image and self.docker_image:
if self.docker_image.is_pvt_dkr_repo and self.docker_image.b64_dkr_config:
self.remove_api_repo_secret(namespace)
self._logger.info(str(deployment))
except client.exceptions.ApiException as e:
self._logger.error(str(e))
def remove_api_ingress(self, namespace):
try:
ing = self.networking_v1_api.delete_namespaced_ingress(name=self._name_ingress, namespace=namespace)
self._logger.info(str(ing))
except client.exceptions.ApiException as e:
self._logger.error(str(e))
def remove_api_service(self, namespace):
try:
service = self.core_v1_api.delete_namespaced_service(name=self._name_service, namespace=namespace)
self._logger.info(service)
except client.exceptions.ApiException as e:
self._logger.error(str(e))
def remove_api_pv_claim(self, namespace):
try:
pv = self.core_v1_api.delete_namespaced_persistent_volume_claim(
name=self._name_pv_claim,
namespace=namespace
)
self._logger.info(str(pv))
except client.exceptions.ApiException as e:
self._logger.error(str(e))
def remove_api_repo_secret(self, namespace):
try:
resp = self.core_v1_api.delete_namespaced_secret(self._name_secret, namespace=namespace)
self._logger.info(str(resp))
return True
except client.exceptions.ApiException as e:
self._logger.error(str(e))
return False
def remove_api_db(self):
db = self.db_server_id
connection = psycopg2.connect(
dbname='postgres',
user=db.master_username,
password=db.master_pass,
host=db.server_url,
port=db.server_port
)
connection.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cursor = connection.cursor()
cursor.execute(f'DROP DATABASE "{self.client_db_name}";')
connection.commit()
connection.close()
# Getters ==========================================================================================================
""" Kubernetes lib api """
@property
def apps_v1_api(self):
return client.AppsV1Api()
@property
def core_v1_api(self):
return client.CoreV1Api()
@property
def networking_v1_api(self):
return client.NetworkingV1Api()
""" Names """
@property
def _name_image(self):
return f'{self.docker_image.name}:{self.docker_image.tag}'
@property
def _name_pv_claim(self):
return f'{self.app_name}-odoo-web-pv-claim'
@property
def _name_deployment(self):
return f'{self.app_name}-odoo-deployment'
@property
def _name_service(self):
return f'{self.app_name}-odoo-service'
@property
def _name_ingress(self):
return f'{self.app_name}-ingress'
@property
def _name_secret(self):
return f'{self.app_name}tls'
@property
def _name_host(self):
return f'{self.sub_domain_name}.{self.domain_name}'
@property
def _logger(self):
return SmileDBLogger(self.env.cr.dbname, self._name, self.id, self._uid)
# Technical ========================================================================================================
def delete_job_task(self):
job_q_env = self.env['queue.job']
jobs = job_q_env.search([
"|", "|", "|",
("state", "=", "pending"),
("state", "=", "enqueued"),
("state", "=", "started"),
("state", "=", "failed"),
('func_string', '=', "kk_odoo_saas.app({0},).post_init_tasks()".format(self.id))])
for job in jobs:
job.button_done()
# TODO
def deploy_apps_from_git(self):
"""
To pull code from github inside running container
"""
try:
cluster_config = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(cluster_config)
except config.config_exception.ConfigException as e:
self._logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
pod = self.core_v1_api.list_namespaced_pod(namespace='default', label_selector='app={}'.format(self.app_name))
if self.is_extra_addon and self.extra_addons and pod and pod.items:
base_version = self.docker_image.base_version
clone_path = "/var/lib/odoo/addons/" + str(base_version)
if self.is_private_repo and self.git_token:
url = self.extra_addons
url = url.replace("http://", "")
url = url.replace("https://", "")
url = url.replace("www.", "")
git_url = "https://oauth2:{0}@{1}".format(self.git_token, url)
else:
git_url = self.extra_addons
is_clone_error = False
error = ''
exec_command = ['git', '-C', clone_path, 'pull']
resp = stream(
self.core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=exec_command,
stderr=True, stdin=True,
stdout=True, tty=False,
_preload_content=False
)
while resp.is_open():
resp.update(timeout=10)
if resp.peek_stdout():
self._logger.info(str(resp.read_stdout()))
if resp.peek_stderr():
is_clone_error = True
error = resp.read_stderr()
self._logger.error(str(error))
break
resp.close()
if is_clone_error:
if error and "not a git repository (or any" in error:
resp1 = stream(
self.core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=['chmod', '-R', 'ugo+rw', clone_path],
stderr=True, stdin=False,
stdout=True, tty=False,
_preload_content=False
)
resp = stream(
self.core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=['git', 'clone', git_url, clone_path],
stderr=True, stdin=False,
stdout=True, tty=False,
_preload_content=False
)
while resp.is_open():
resp.update(timeout=25)
if resp.peek_stdout():
self._logger.info(str(resp.read_stdout()))
if resp.peek_stderr():
error = resp.read_stderr()
self._logger.error(str(error))
else:
self._logger.info(str(
"No Response"
))
resp.close()
else:
return False

80
kk_odoo_saas/models/cluster.py Executable file
View File

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class SaasK8sClusterNS(models.Model):
_name = 'kk_odoo_saas.app.cluster.ns'
_description = 'SaaS Cluster NameSpace'
name = fields.Char()
status = fields.Char()
age = fields.Char()
all_json = fields.Text('Complete json')
class SaasK8sClusterPod(models.Model):
_name = 'kk_odoo_saas.app.cluster.pod'
_description = 'SaaS Cluster Pod'
name = fields.Char()
ns = fields.Char()
ready = fields.Char()
status = fields.Char()
restarts = fields.Char()
age = fields.Char()
all_json = fields.Text('Complete json')
class SaasK8sClusterDeployment(models.Model):
_name = 'kk_odoo_saas.app.cluster.deployment'
_description = 'SaaS Cluster Deployment'
name = fields.Char()
ns = fields.Char()
ready = fields.Char()
age = fields.Char()
all_json = fields.Text('Complete json')
class SaasK8sClusterIngress(models.Model):
_name = 'kk_odoo_saas.app.cluster.ingress'
_description = 'SaaS Cluster Ingress'
name = fields.Char()
ns = fields.Char()
hosts = fields.Char()
ing_class = fields.Char()
addresses = fields.Char()
ports = fields.Char()
age = fields.Char()
all_json = fields.Text('Complete json')
class SaasK8sClusterService(models.Model):
_name = 'kk_odoo_saas.app.cluster.service'
_description = 'SaaS Cluster Service'
name = fields.Char()
type_ = fields.Char()
cluster_ip = fields.Char()
external_ip = fields.Char()
ports = fields.Char()
age = fields.Char()
all_json = fields.Text('Complete json')
class SaasK8sClusterPV(models.Model):
_name = 'kk_odoo_saas.app.cluster.pv'
_description = 'SaaS Cluster PV'
name = fields.Char()
capacity = fields.Char()
access_modes = fields.Char()
reclaim_policy = fields.Char()
status = fields.Char()
claim = fields.Char()
storage_class = fields.Char()
reason = fields.Char()
age = fields.Char()
all_json = fields.Text('Complete json')

135
kk_odoo_saas/models/k8s_config.py Executable file
View File

@ -0,0 +1,135 @@
import psycopg2
from odoo import models, fields, api
from kubernetes import config, client
from odoo.exceptions import UserError
import yaml
import logging
_logger = logging.getLogger(__name__)
class SaaSAppK8sConfig(models.Model):
_name = 'kk_odoo_saas.k8s.config'
_description = 'Kubernetes Cluster Configuration'
name = fields.Char()
config_file = fields.Text('Yaml Configuration File')
cluster_name = fields.Char('')
namespaces = fields.Text('Namespaces in Cluster')
domain_name = fields.Char('Domain Name')
def check_connectivity(self):
if self.config_file:
try:
data2 = yaml.safe_load(self.config_file)
config.load_kube_config_from_dict(data2)
v1 = client.CoreV1Api()
response = v1.list_namespace()
nss = []
for namespace in response.items:
nss.append(namespace.metadata.name)
nl = '\n'
self.namespaces = nl.join(nss)
except config.config_exception.ConfigException as e:
raise UserError("Unable to Connect K8s Cluster")
else:
raise UserError("Please Add config file")
def validate_domain_name(self):
if self.domain_name:
pass
def get_default_config(self):
for conf in self.search([], limit=1):
return conf
def update_cluster_nodes(self):
if self.config_file:
try:
data2 = yaml.safe_load(self.config_file)
config.load_kube_config_from_dict(data2)
v1 = client.CoreV1Api()
response = v1.list_node()
node_env = self.env['kk_odoo_saas.k8s.node']
if response:
node_env.search([]).unlink()
for node in response.items:
node_env.create({'name': node.metadata.name, 'labels': str(node.metadata.labels), 'annotations': str(node.metadata.annotations), 'taints': str(node.spec.taints), 'yaml_info': str(node)})
except config.config_exception.ConfigException as e:
_logger.error(e)
raise UserError("Unable to Connect K8s Cluster")
else:
raise UserError("Please Add config file")
class DockerImages(models.Model):
_name = 'kk_odoo_saas.k8s.docker.images'
name = fields.Char('Image Name', required=True)
tag = fields.Char('Tag Name', required=True, default='latest')
description = fields.Char('Description')
is_pvt_dkr_repo = fields.Boolean('Using Private Docker Repository')
b64_dkr_config = fields.Text('base64 docker config json file')
repo_link = fields.Char('Related Repository')
base_version = fields.Selection([
('14.0', '14.0'),
('15.0', '15.0'),
('16.0', '16.0'),
('17.0', '17.0'),
('18.0', '18.0'),
], required=True)
# base_version is for pulling git code in folder e.g /var/lib/odoo/addons/14.0 etc.
gevent_key = fields.Selection([
('--longpolling-port', '--longpolling-port'),
('--gevent-port', '--gevent-port'),
], required=True, default='--gevent-port')
@api.depends('name', 'tag')
def name_get(self):
res = []
for record in self:
name = record.name
if record.tag:
name = name + ':' + record.tag
res.append((record.id, name))
return res
class Node(models.Model):
_name = 'kk_odoo_saas.k8s.node'
name = fields.Char('Node Name')
labels = fields.Text('Labels')
annotations = fields.Text()
taints = fields.Text()
yaml_info = fields.Text("Yaml Description")
class MasterDbCreds(models.Model):
_name = 'kk_odoo_saas.k8s.master_db_creds'
name = fields.Char('DB Server Name')
master_username = fields.Char('Master Username', default='postgres', required=True)
master_pass = fields.Char('Master Password', required=True)
server_url = fields.Char('Server URL', required=True)
server_port = fields.Char('Server Port', default='5432', required=True)
status = fields.Selection([('connected', 'Connected'), ('not_connected', 'Not Connected')], default='not_connected')
def check_connectivity(self):
for rec in self:
if rec.master_username and rec.master_pass and rec.server_url:
try:
conn = psycopg2.connect(database='postgres',
user=rec.master_username,
password=rec.master_pass,
host=rec.server_url,
port=rec.server_port or 5432)
if conn:
_logger.info("Connected to PG DB server")
self.status = 'connected'
return conn
except Exception as e:
_logger.exception(e)
self.status = 'not_connected'
raise UserError('Unable to Connect Postgres.\nPlease Check Postgres Credentials...!')
else:
self.status = 'not_connected'
raise UserError('Please Enter Postgres Credentials...!')

482
kk_odoo_saas/models/models.py Executable file
View File

@ -0,0 +1,482 @@
# -*- coding: utf-8 -*-
import base64
import psycopg2
import requests
import os
from odoo import models, fields, api, _
from ..utils import k8s_deployment as k8s
from ..utils import ingress, logs
from ..utils import del_git_code as dc
import re
from odoo.exceptions import ValidationError, MissingError
from odoo.addons.smile_log.tools import SmileDBLogger
import logging
import xmlrpc.client
import random
import string
from odoo.addons.queue_job.exception import RetryableJobError
from odoo.exceptions import AccessError
from datetime import timedelta
from ..utils import pg_server as pgx
_logger = logging.getLogger(__name__)
class SaaSAppSslSecret(models.Model):
_name = 'kk_odoo_saas.app.ssl_secret'
name = fields.Char('Secret Name')
class SaaSApp(models.Model):
_name = 'kk_odoo_saas.app'
_description = 'SaaS App'
_inherit = ['mail.thread', 'mail.activity.mixin', 'kk_odoo_saas.kubernetes.api']
app_name = fields.Char('App Unique Id',
default=lambda self: self.env['ir.sequence'].next_by_code('kk_odoo_saas.app'), tracking=True, copy=False)
name = fields.Char(tracking=True)
is_custom_image = fields.Boolean(default=True)
docker_image = fields.Many2one('kk_odoo_saas.k8s.docker.images')
is_pvt_dkr_repo = fields.Boolean('Using Private Docker Repository')
is_extra_addon = fields.Boolean('Use Extra Addons')
extra_addons = fields.Char('Git Url', tracking=True)
is_private_repo = fields.Boolean('Is Private Repository?')
git_token = fields.Char('Auth Token')
# K8s values
client = fields.Many2one('res.partner', related='admin_user.partner_id', tracking=True)
country_id = fields.Many2one(string="Country", comodel_name='res.country',
help="Country for which this instance is being deployed")
admin_user = fields.Many2one("res.users", "Client User", tracking=True)
def _get_default_cluster_config(self):
cluster = self.env['kk_odoo_saas.k8s.config'].search([], limit=1)
if cluster:
return cluster.id
return False
def _get_default_db_server(self):
db_server = self.env['kk_odoo_saas.k8s.master_db_creds'].search([], limit=1)
if db_server:
return db_server
return False
configuration = fields.Many2one(
'kk_odoo_saas.k8s.config', string='Configuration', default=_get_default_cluster_config)
domain_name = fields.Char(related='configuration.domain_name')
sub_domain_name = fields.Char(required=True)
is_dedicated_node = fields.Boolean(string='Any Dedicated Node')
node_id = fields.Many2one('kk_odoo_saas.k8s.node', string='Node')
node_key = fields.Char()
node_value = fields.Char()
demo_data = fields.Boolean('Install Demo Data')
status = fields.Selection([('d', 'Draft'), ('l', 'Launched'), ('m', 'Modified'), ('del', 'Deleted')],
string='Status', default='d', tracking=True)
expiry_date = fields.Date(tracking=True)
subscription_id = fields.Many2one('sale.subscription', string='Related Subscription', tracking=True)
notes = fields.Text()
module_ids = fields.Many2many(comodel_name='saas.app', string='Modules to install')
login_email = fields.Char('Login Email')
login_pwd = fields.Char('Login Pwd')
master_login_email = fields.Char('Master Login Email')
master_login_pwd = fields.Char('Master Login Pwd')
custom_domain_ids = fields.One2many('saas.app.custom.domain', 'saas_app_id', string='Custom Domains')
def _default_db_name(self):
return self.sub_domain_name
k8s_logs = fields.Many2many('smile.log', string='K8s Logs', compute='_get_k8s_logs')
# db server relation
db_server_id = fields.Many2one(
'kk_odoo_saas.k8s.master_db_creds', string="DB Server", default=_get_default_db_server)
client_db_name = fields.Char("Database Name", required=True)
login_url = fields.Char('Login URL', compute='_get_instance_login_url')
# _sql_constraints = [
# ('hostname_uniq', 'unique(hostname)', "A Domain already exists. Domain's name must be unique!"),
# ]
@api.model
def create(self, values):
_logger = logging.getLogger(__name__)
if self:
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
res = super(SaaSApp, self).create(values)
if not res.validate_domain_name():
_logger.error('Either Domain or Subdomain is not valid')
raise ValidationError('Either Domain or Subdomain is not valid')
return res
def write(self, vals):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
# if vals and 'status' not in vals and self.status not in ['d', 'del']:
# vals.update({'status': 'm'})
res = super(SaaSApp, self).write(vals)
if 'custom_domain_ids' in vals:
#todo: add validation, limit number of domains per instance
self.update_app()
if 'sub_domain_name' or 'domain_name' in vals:
if not self.validate_domain_name():
_logger.error('Either Domain or Subdomain is not valid')
raise ValidationError('Either Domain or Subdomain is not valid')
return res
@api.onchange('app_name')
def set_sub_domain_name(self):
self.sub_domain_name = self.app_name
# also set the database name
self.client_db_name = self.app_name
def validate_domain_name(self):
if self.domain_name and self.sub_domain_name:
full_name = f'{self.sub_domain_name}.{self.domain_name}'
domain_regex = r'(([\da-zA-Z])([_\w-]{,62})\.){,127}(([\da-zA-Z])[_\w-]{,61})?([\da-zA-Z]\.((xn\-\-[a-zA-Z\d]+)|([a-zA-Z\d]{2,})))'
domain_regex = '{0}$'.format(domain_regex)
valid_domain_name_regex = re.compile(domain_regex, re.IGNORECASE)
full_name = full_name.lower().strip()
if re.match(valid_domain_name_regex, full_name):
return True
return
def deploy_app(self):
self.deploy_api_main(namespace='default')
self.status = 'l'
self.with_delay().post_init_tasks()
def delete_app_from_wizard(self, delete_db, delete_pv, delete_svc, delete_ing, delete_deployment):
self.remove_api_main(delete_db, delete_pv, delete_svc, delete_ing, delete_deployment, namespace='default')
self.status = 'del'
def update_app(self):
k8s.update_app(self)
self.status = 'l'
def get_url(self):
return f'http://{self.sub_domain_name}.{self.domain_name}'
def deploy_apps_from_git(self):
k8s.deploy_apps_from_git(self)
def restart_odoo_service(self):
k8s.restart_odoo_service(self)
def action_show_subscription(self):
self.ensure_one()
assert self.subscription_id, "This app is not associated with any Subscription"
return {
"type": "ir.actions.act_window",
"name": "Subscription",
"res_model": "sale.subscription",
"res_id": self.subscription_id.id,
"view_mode": "form",
}
def action_create_subscription(self):
self.ensure_one()
assert not self.subscription_id, "This app is already associated with Subscription"
return {
"type": "ir.actions.act_window",
"name": "Subscription",
"res_model": "sale.subscription",
"view_mode": "form",
"context": {
"default_name": self.name + "'s SaaS Subscription",
"default_build_id": self.id,
"default_partner_id": self.client.id,
}
}
def _get_instance_login_url(self):
for app in self:
app.login_url = ''
response, db = pgx.get_admin_credentials(app)
if response and db:
app.login_url = f'https://{self.sub_domain_name}.{self.domain_name}/saas/login?db={db}&login={response[0][0]}&passwd={response[0][1]}'
else:
_logger.info("Unknown Error!")
def action_connect_instance(self):
self.ensure_one()
response, db = pgx.get_admin_credentials(self)
if response and db:
login = response[0][0]
password = response[0][1]
login_url = "https://{self.sub_domain_name}.{self.domain_name}/saas/login?db={db}&login={login}&passwd={password}"
_logger.info("Login URL %r " % (login_url))
return {
'type': 'ir.actions.act_url',
'url': login_url,
'target': 'new',
}
else:
_logger.info("Unknown Error!")
def create_instance_admin_user_for_client(self, models1, db, uid, password, client_pwd):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
adm_user_id = models1.execute_kw(db, uid, password,
'res.users', 'search_read',
[[['login', '=', 'admin']]], {'fields': ['groups_id'], 'limit': 1}
)
if adm_user_id:
adm_user_id = adm_user_id[0]
groups_ids = adm_user_id.get('groups_id', False)
if groups_ids:
new_user_id = models1.execute_kw(db, uid, password, 'res.users', 'create',
[{'name': self.client.name,
'login': self.client.email,
'company_ids': [1], 'company_id': 1,
'password': client_pwd}])
if new_user_id:
_logger.info('Created client Account on instance')
return models1.execute_kw(db, uid, password, 'res.groups', 'write',
[groups_ids, {'users': [(4, new_user_id)]}])
except xmlrpc.client.Error as e:
_logger.error(str(e))
return
def reset_apps_admin_pwd(self):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
protocol = 'https'
url = f'{protocol}://{self.sub_domain_name}.{self.domain_name}'
db = self.sub_domain_name
new_pwd_client = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10))
new_pwd_master = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10))
username = 'admin'
password = 'admin'
try:
common = xmlrpc.client.ServerProxy('{}/xmlrpc/2/common'.format(url))
uid = common.authenticate(db, username, password, {})
_logger.info('Sending request to app with uid {}'.format(uid))
models1 = xmlrpc.client.ServerProxy('{}/xmlrpc/2/object'.format(url))
try:
if self.create_instance_admin_user_for_client(models1, db, uid, password, new_pwd_client):
_logger.info('Updated Client User\'s Access Rights on instance')
self.update({'login_pwd': new_pwd_client, 'login_email': self.client.email})
adm_user_id = models1.execute_kw(
db, uid, password, 'res.users', 'search',
[[['login', '=', 'admin']]], {'limit': 1})[0]
if models1.execute_kw(db, uid, password, 'res.users', 'write', [[adm_user_id], {
'password': new_pwd_master,
}]):
self.update({'master_login_pwd': new_pwd_master, 'master_login_email': 'admin'})
self.send_app_pwd_cred_email()
_logger.info('Password and login changed Successfully')
except xmlrpc.client.Error as e:
_logger.error(str(e))
except xmlrpc.client.Error as e:
_logger.error(str(e))
def set_user_country(self):
country_code = self.country_id.code
if self.country_id and self.country_id.code:
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
protocol = 'https'
url = f'{protocol}://{self.sub_domain_name}.{self.domain_name}'
db = self.sub_domain_name
username = 'admin'
password = 'admin'
try:
common = xmlrpc.client.ServerProxy('{}/xmlrpc/2/common'.format(url))
uid = common.authenticate(db, username, password, {})
_logger.info('Sending request to app with uid {}'.format(uid))
models1 = xmlrpc.client.ServerProxy('{}/xmlrpc/2/object'.format(url))
try:
country = models1.execute_kw(db, uid, password,
'res.country', 'search_read',
[[['code', 'ilike', country_code]]], {'fields': ['id'], 'limit': 1}
)
if country:
country = country[0]
if country:
new_user_id = models1.execute_kw(db, uid, password, 'res.company', 'write',
[[1], {'country_id': country_code and self.country_id.id,
'currency_id': country_code and self.country_id.currency_id.id}])
if new_user_id:
_logger.info('Updated country of the user')
except xmlrpc.client.Error as e:
_logger.error(str(e))
except xmlrpc.client.Error as e:
_logger.error(str(e))
def post_init_tasks(self):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
if not self.check_site_assible():
_logger.info('Waiting for the App to become live....')
raise RetryableJobError('Unable to get the app live.')
else:
self.set_user_country()
self.reset_apps_admin_pwd()
def check_site_assible(self):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
resp = requests.get(f'http://{self.sub_domain_name}.{self.domain_name}')
_logger.info('App, sent this status code {}'.format(resp.status_code))
if resp.status_code == 200:
return True
else:
return False
except Exception as e:
_logger.error(str(e))
return False
def send_app_pwd_cred_email(self):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
template = False
try:
template = self.env.ref('kk_odoo_saas.app_invitation_email', raise_if_not_found=False)
except ValueError:
pass
assert template._name == 'mail.template'
template_values = {
'email_to': '${object.admin_user.email|safe}',
'email_cc': False,
'auto_delete': True,
'partner_to': False,
'scheduled_date': False,
}
template.write(template_values)
if not self.admin_user.email:
_logger.error(_("Cannot send email: user %s has no email address.", self.admin_user.name))
with self.env.cr.savepoint():
try:
template.send_mail(self.id, force_send=True, raise_exception=True)
except Exception as e:
_logger.error(str(e))
_logger.info(_("App Details email sent for user <%s> to <%s>", self.admin_user.login, self.admin_user.email))
def _message_get_suggested_recipients(self):
recipients = super(SaaSApp, self)._message_get_suggested_recipients()
try:
for saas_app in self:
if saas_app.client:
saas_app._message_add_suggested_recipient(recipients, partner=saas_app.client, reason=_('SaaS Client'))
except AccessError: # no read access rights -> just ignore suggested recipients because this imply modifying followers
pass
return recipients
def _get_k8s_logs(self):
for app in self:
app.k8s_logs = False
logs_ = self.env['smile.log'].search([('res_id', '=', app.id), ('model_name', '=', self._name)])
for log in logs_:
app.k8s_logs = [(4, log.id)]
def get_pod_logs(self):
output = logs.read_logs(app_name=self.app_name, config_file=self.configuration.config_file, self=self, tail_lines=None)
if output:
result = base64.b64encode(output.encode())
attachment_obj = self.env['ir.attachment']
# create attachment
attachment_id = attachment_obj.create(
{'name': self.app_name+'-odoo-logs.log', 'datas': result, 'public': False})
# prepare download url
download_url = '/web/content/' + str(attachment_id.id) + '?download=true'
# download
return {
"type": "ir.actions.act_url",
"url": str(download_url),
"target": "new",
}
else:
raise MissingError('Unable to get logs \nReason: Running Pod / Container not found')
def action_log_viewer(self):
return {
"type": "ir.actions.act_url",
"url": "/saas/instance/{app_id}".format(app_id=self.id),
"target": "new",
}
def get_timed_pod_logs(self, interval=None, since_seconds=None, previous=None, tail_lines=None):
output = logs.read_logs(app_name=self.app_name, config_file=self.configuration.config_file, self=self, since_seconds=since_seconds)
if output:
return output
def update_docker_image(self, container_arguments, env_vars=False):
patched_deployment = k8s.update_deployment(self=self, container_arguments=container_arguments, env_vars=env_vars)
if patched_deployment:
self.env['bus.bus'].sendone(
(self._cr.dbname, 'res.partner', self.env.user.partner_id.id),
{'type': 'simple_notification', 'title': 'Image Update in Progress',
'message': 'Deployment in Progress with latest docker image'}
)
return True
def get_odoo_deployment(self):
deployment = k8s.read_deployment(self=self)
if deployment:
return deployment
return False
def refresh_node_list(self):
if self.configuration:
self.configuration.update_cluster_nodes()
def get_pg_db_connection(self, db='postgres'):
for rec in self:
if rec.db_server_id.master_pass and rec.db_server_id.master_username and rec.db_server_id.server_url:
try:
_logger.info("Going to connect to PG DB server")
conn = psycopg2.connect(database=db,
user=rec.db_server_id.master_username,
password=rec.db_server_id.master_pass,
host=rec.db_server_id.server_url,
port=rec.db_server_id.server_port or 5432
)
if conn:
_logger.info("Connected to PG DB server")
return conn
except Exception as e:
_logger.exception(e)
return
else:
return
def del_git_dir(self):
base_version = self.docker_image.base_version
delete_path = "/var/lib/odoo/addons/" + str(base_version)
dc.del_git_dir(self, path=delete_path)
class SaaSAppDomain(models.Model):
_name = 'saas.app.custom.domain'
_description = 'SaaS App Custom Domain'
name = fields.Char('Domain Name', required=True)
saas_app_id = fields.Many2one('kk_odoo_saas.app')
ssl = fields.Boolean('Enable SSL?', default=True)
class DockerAccount(models.Model):
_name = 'saas.docker.hub.account'
username = fields.Char('docker hub username')
pwd = fields.Char('Password or Access Token')
# for more info https://docs.docker.com/docker-hub/access-tokens/

View File

@ -0,0 +1,16 @@
from odoo import fields, models, api
class ProductTemplate(models.Model):
_inherit = "product.template"
saas_app_id = fields.Many2one("saas.app", ondelete="cascade", index=True)
saas_package_id = fields.Many2one("saas.package", ondelete="cascade", index=True)
is_saas_product = fields.Boolean("Is SaaS product?", default=False)
@api.model
def create(self, vals):
if vals.get("is_saas_product"):
vals["taxes_id"] = [(5,)]
vals["supplier_taxes_id"] = [(5,)]
return super(ProductTemplate, self).create(vals)

View File

@ -0,0 +1,34 @@
from odoo import api, fields, models
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
show_packages = fields.Boolean(
"Show packages", config_parameter="kk_odoo_saas.show_packages"
)
show_apps = fields.Boolean("Show apps", config_parameter="kk_odoo_saas.show_apps")
show_buy_now_button = fields.Boolean(
"Show 'Buy now' button", config_parameter="kk_odoo_saas.show_buy_now_button"
)
show_try_trial_button = fields.Boolean(
"Show 'Try trial' button", config_parameter="kk_odoo_saas.show_try_trial_button"
)
@api.model
def get_values(self):
res = super(ResConfigSettings, self).get_values()
select_type = self.env["ir.config_parameter"].sudo()
packages = select_type.get_param("kk_odoo_saas.show_packages")
apps = select_type.get_param("kk_odoo_saas.show_apps")
buy_now_button = select_type.get_param("kk_odoo_saas.show_buy_now_button")
try_trial_button = select_type.get_param("kk_odoo_saas.show_try_trial_button")
# fmt: off
res.update({
"show_packages": packages,
"show_apps": apps,
"show_buy_now_button": buy_now_button,
"show_try_trial_button": try_trial_button,
})
# fmt: on
return res

View File

View File

@ -0,0 +1,102 @@
import logging
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class SaasApp(models.Model):
_name = "saas.app"
_description = "SaaS App"
_order = "name"
_inherit = "saas.period.product.mixin"
name = fields.Char("Technical Name", required=True, index=True)
shortdesc = fields.Char("Module Name", required=True)
dependency_ids = fields.Many2many("saas.app", "saas_apps_dependency_rel", "dep_id", "app_id", string="Dependencies")
icon_image = fields.Binary("Icon")
allow_to_sell = fields.Boolean(default=True, string="Sellable")
@api.model
def create(self, vals):
res = super(SaasApp, self).create(vals)
if not res.product_tmpl_id:
res.product_tmpl_id = self.env["product.template"].create({
"name": res.shortdesc,
"image_1920": res.icon_image,
"saas_app_id": res.id,
"is_saas_product": True,
"type": 'service',
"purchase_ok": False,
"subscription_template_id": self.env.ref("sale_subscription.monthly_subscription").id,
"recurring_invoice": True,
"website_published": True,
"list_price": 0,
})
return res
def write(self, vals):
res = super(SaasApp, self).write(vals)
if vals.get('month_price', None) is not None or vals.get('year_price', None) is not None:
self._update_variant_prices()
return res
def _update_variant_prices(self):
for app in self:
for variant in app.product_tmpl_id.product_variant_ids:
for attr in variant.product_template_attribute_value_ids:
if attr.name == "Monthly":
attr.update({'price_extra': app.month_price})
if attr.name == "Annually":
attr.update({'price_extra': app.year_price})
def _search_or_create(self, ir_module):
app = self.search([("name", "=", ir_module.name)])
if not app:
app = self.env["saas.app"].create({
"name": ir_module.name,
"shortdesc": ir_module.shortdesc,
"icon_image": ir_module.icon_image
})
return app
def dependencies_str(self):
self.ensure_one()
visited_saas_module_ids = set()
def make_list(deps):
result = []
for dep in deps:
if dep.id in visited_saas_module_ids:
continue
visited_saas_module_ids.add(dep.id)
result += [dep.name] + make_list(dep.dependency_ids)
return result
return ",".join(make_list(self.dependency_ids))
@api.model
def action_make_applist_from_local_instance(self):
for x in map(self.browse, self._search([])):
x.unlink()
def walk(parent_ir_module_name, parent_app_name=None):
modules = self.env["ir.module.module.dependency"].sudo().search([("name", "=", parent_ir_module_name)]).mapped("module_id")
for m in modules:
app_name = None
if m.application:
app = self.env["saas.app"]._search_or_create(m)
if parent_app_name:
app.dependency_ids |= self.env["saas.app"].search([("name", "=", parent_app_name)])
app_name = app.name
else:
app_name = parent_app_name
walk(m.name, app_name)
walk("base")

View File

@ -0,0 +1,61 @@
import logging
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError, UserError
_logger = logging.getLogger(__name__)
class SaasPackage(models.Model):
_name = "saas.package"
_inherit = ["saas.period.product.mixin"]
is_published = fields.Boolean("Publish It?", default=True)
package_image = fields.Image(
string='Package image'
)
name = fields.Char(copy=False)
module_ids = fields.Many2many('saas.app', string="Modules to install")
docker_image = fields.Many2one('kk_odoo_saas.k8s.docker.images', 'Related Docker Image')
stripe_product_id = fields.Char('Stripe Id')
subscription_template = fields.Many2one('sale.subscription.template')
@api.model
def create(self, vals):
res = super(SaasPackage, self).create(vals)
if not res.product_tmpl_id:
res.product_tmpl_id = self.env["product.template"].create({
"name": res.name,
"image_1920": res.package_image,
"saas_package_id": res.id,
"is_saas_product": True,
"type": 'service',
"purchase_ok": False,
"subscription_template_id": self.env.ref("sale_subscription.monthly_subscription").id,
"recurring_invoice": True,
"website_published": True,
"list_price": 0,
})
return res
def write(self, vals):
res = super(SaasPackage, self).write(vals)
if vals.get('month_price', None) is not None or vals.get('year_price', None) is not None:
self._update_variant_prices()
return res
def _update_variant_prices(self):
for app in self:
for variant in app.product_tmpl_id.product_variant_ids:
for attr in variant.product_template_attribute_value_ids:
if attr.name == "Monthly":
attr.update({'price_extra': app.month_price})
if attr.name == "Annually":
attr.update({'price_extra': app.year_price})
def refresh_page(self):
# Empty-function for purpose of refreshing page
pass

View File

@ -0,0 +1,54 @@
from odoo import api, models, fields
class SaasPeriodProductMixin(models.AbstractModel):
_name = "saas.period.product.mixin"
_description = "Period Product Mixin"
product_tmpl_id = fields.Many2one("product.template", ondelete="cascade", readonly=True)
month_product_id = fields.Many2one("product.product", string="Product for monthly subscription", compute="_compute_product_ids", store=True)
year_product_id = fields.Many2one("product.product", string="Product for annually subscription", compute="_compute_product_ids", store=True)
currency_id = fields.Many2one("res.currency", related="product_tmpl_id.currency_id")
# TODO: when following fields are written, you need to update prices on product.product
month_price = fields.Float("Month price", default=0.0)
year_price = fields.Float("Year price", default=0.0)
@api.depends("product_tmpl_id")
def _compute_product_ids(self):
patvs_month = self.env.ref("kk_odoo_saas.product_attribute_value_subscription_monthly")
patvs_year = self.env.ref("kk_odoo_saas.product_attribute_value_subscription_annually")
attr = self.env.ref("kk_odoo_saas.product_attribute_subscription")
for app in self:
if not app.product_tmpl_id:
app.month_product_id = app.year_product_id = self.env["product.product"]
continue
line = self.env["product.template.attribute.line"].sudo().search([
("product_tmpl_id", "=", app.product_tmpl_id.id),
("attribute_id", "=", attr.id),
])
if not line:
line = line.create({
"product_tmpl_id": app.product_tmpl_id.id,
"attribute_id": attr.id,
"value_ids": [(6, 0, [
patvs_year.id, patvs_month.id,
])]
})
ptv_ids = line.product_template_value_ids
month_ptv = ptv_ids.filtered(lambda x: x.product_attribute_value_id == patvs_month)
month_ptv.write({
"price_extra": app.month_price
})
app.month_product_id = month_ptv.ptav_product_variant_ids[:1]
year_ptv = ptv_ids.filtered(lambda x: x.product_attribute_value_id == patvs_year)
year_ptv.write({
"price_extra": app.year_price
})
app.year_product_id = year_ptv.ptav_product_variant_ids[:1]

View File

@ -0,0 +1,86 @@
from odoo import fields, models
import logging
_logger = logging.getLogger(__name__)
class SaleOrder(models.Model):
_inherit = 'sale.order'
build_id = fields.Many2one("kk_odoo_saas.app")
is_pkg_pdt = fields.Boolean(default=False)
def _split_subscription_lines(self):
"""Split the order line according to subscription templates that must be created."""
self.ensure_one()
res = dict()
for line in self.order_line:
if line.product_id:
for p_id, p_name in line.product_id.name_get():
if '(Annually)' in p_name:
line.product_id.update(
{'subscription_template_id': self.env.ref('sale_subscription.yearly_subscription').id})
elif '(Monthly)' in p_name:
line.product_id.update(
{'subscription_template_id': self.env.ref('sale_subscription.monthly_subscription').id})
new_sub_lines = self.order_line.filtered(lambda
l: not l.subscription_id and l.product_id.subscription_template_id and l.product_id.recurring_invoice)
templates = new_sub_lines.mapped('product_id').mapped('subscription_template_id')
for template in templates:
lines = self.order_line.filtered(
lambda l: l.product_id.subscription_template_id == template and l.product_id.recurring_invoice)
res[template] = lines
return res
def _action_confirm(self):
"""Update and/or create subscriptions on order confirmation."""
res = super(SaleOrder, self)._action_confirm()
# self.create_saas_app_from_subscription()
return res
def create_saas_app_from_subscription(self):
for so in self:
lines = so.order_line.filtered(lambda l: l.subscription_id is not False)
p_ids = so.order_line.mapped('product_id')
if lines and p_ids:
saas_app_ids = [app.id for app in self.env['saas.app'].search([('year_product_id', 'in', p_ids.ids)])]
if not saas_app_ids:
saas_app_ids = [app.id for app in self.env['saas.app'].search([('month_product_id', 'in', p_ids.ids)])]
line = lines[0]
sub_id = line.subscription_id
pkg = False
if so.is_pkg_pdt:
pkg = self.env['saas.package'].search([('year_product_id', 'in', p_ids.ids)])
if not pkg:
pkg = self.env['saas.package'].search([('month_product_id', 'in', p_ids.ids)])
if pkg:
saas_app_ids = pkg.module_ids.ids
if so and so.build_id and sub_id:
so.build_id.update({'subscription_id': sub_id.id,
'module_ids': [(6, 0, saas_app_ids)]
})
sub_id.build_id = so.build_id
so.build_id.deploy_app()
else:
saas_app_env = self.env['kk_odoo_saas.app']
def_vals = saas_app_env.default_get(fields_list=['app_name', ])
if self.partner_id.user_ids:
def_vals['admin_user'] = self.partner_id.user_ids.ids[0]
configurations = self.env["kk_odoo_saas.k8s.config"]
config = configurations.get_default_config()
if config:
def_vals['configuration'] = config.id
def_vals['sub_domain_name'] = def_vals.get('app_name')
def_vals['subscription_id'] = sub_id.id
def_vals['module_ids'] = [(6, 0, saas_app_ids)]
def_vals['docker_image'] = pkg.docker_image.id
def_vals['name'] = '{}\'s SaaS App'.format(self.partner_id.name)
saas_app = saas_app_env.create(def_vals)
sub_id.build_id = saas_app.id
self.build_id = saas_app.id
_logger.info('Going to Deploy SaaS App, Subscription is going to start')
saas_app.deploy_app()
else:
_logger.error('Cant create SaaS App, No K8s configuration found')

View File

@ -0,0 +1,20 @@
from odoo import fields, models, api
import logging
_logger = logging.getLogger(__name__)
class SaleSubscription(models.Model):
_inherit = 'sale.subscription'
build_id = fields.Many2one("kk_odoo_saas.app", string="Related SaaS Instance")
is_saas = fields.Boolean('Is SaaS Subscription')
def start_subscription(self):
res = super(SaleSubscription, self).start_subscription()
if self.build_id:
self.build_id.deploy_app()
return res
@api.model
def create(self, vals):
res = super(SaleSubscription, self).create(vals)
return res

View File

@ -0,0 +1,17 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_saas_app_client,kk_odoo_saas.kk_odoo_saas,model_kk_odoo_saas_app,kk_odoo_saas.group_saas_client,1,0,0,0
access_kk_odoo_saas_k8s_config,kk_odoo_saas.kk_odoo_saas_k8s_config,model_kk_odoo_saas_k8s_config,base.group_user,1,1,1,1
access_kk_odoo_saas_k8s_docker_images_config,kk_odoo_saas.kk_odoo_saas_docker_images_config,model_kk_odoo_saas_k8s_docker_images,base.group_user,1,1,1,1
access_saas_app_user,access_saas_app_user,model_saas_app,base.group_user,1,1,1,1
access_saas_app_admin,access_saas_app_admin,model_saas_app,kk_odoo_saas.group_saas_manager,1,1,1,1
access_kk_odoo_saas_k8s_node,access_kk_odoo_saas.k8s.node,model_kk_odoo_saas_k8s_node,kk_odoo_saas.group_saas_manager,1,1,1,1
access_kk_odoo_saas_app_delete_wizard,access_saas_app_delete_wizard,model_kk_odoo_saas_app_delete_wizard,kk_odoo_saas.group_saas_manager,1,1,1,1
access_kk_odoo_saas_app_update_dkr_img_wizard,access_kk_odoo_saas_app_update_dkr_img_wizard,model_kk_odoo_saas_app_update_dkr_img_wizard,kk_odoo_saas.group_saas_manager,1,1,1,1
access_saas_app_secret_admin,access_saas_app_secret_admin,model_kk_odoo_saas_app_ssl_secret,kk_odoo_saas.group_saas_manager,1,1,1,1
access_saas_app_custom_domain,admin_access_saas_app_domain,model_saas_app_custom_domain,kk_odoo_saas.group_saas_manager,1,1,1,1
access_saas_app_custom_domain_portal,portal_access_saas_app_domain,model_saas_app_custom_domain,base.group_portal,1,0,0,0
access_kk_odoo_saas_master_db_creds,kk_odoo_saas.kk_odoo_saas_master_db_creds,model_kk_odoo_saas_k8s_master_db_creds,kk_odoo_saas.group_saas_manager,1,1,1,1
access_saas_package_admin,access_saas_package_admin,model_saas_package,kk_odoo_saas.group_saas_manager,1,1,1,1
access_saas_app_container_argument,access_saas_app_container_argument,model_saas_app_container_argument,kk_odoo_saas.group_saas_manager,1,1,1,1
access_saas_app_container_env_var,access_saas_app_container_env_var,model_saas_app_container_env_var,kk_odoo_saas.group_saas_manager,1,1,1,1
access_saas_app_manager,kk_odoo_saas.kk_odoo_saas,model_kk_odoo_saas_app,kk_odoo_saas.group_saas_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_saas_app_client kk_odoo_saas.kk_odoo_saas model_kk_odoo_saas_app kk_odoo_saas.group_saas_client 1 0 0 0
3 access_kk_odoo_saas_k8s_config kk_odoo_saas.kk_odoo_saas_k8s_config model_kk_odoo_saas_k8s_config base.group_user 1 1 1 1
4 access_kk_odoo_saas_k8s_docker_images_config kk_odoo_saas.kk_odoo_saas_docker_images_config model_kk_odoo_saas_k8s_docker_images base.group_user 1 1 1 1
5 access_saas_app_user access_saas_app_user model_saas_app base.group_user 1 1 1 1
6 access_saas_app_admin access_saas_app_admin model_saas_app kk_odoo_saas.group_saas_manager 1 1 1 1
7 access_kk_odoo_saas_k8s_node access_kk_odoo_saas.k8s.node model_kk_odoo_saas_k8s_node kk_odoo_saas.group_saas_manager 1 1 1 1
8 access_kk_odoo_saas_app_delete_wizard access_saas_app_delete_wizard model_kk_odoo_saas_app_delete_wizard kk_odoo_saas.group_saas_manager 1 1 1 1
9 access_kk_odoo_saas_app_update_dkr_img_wizard access_kk_odoo_saas_app_update_dkr_img_wizard model_kk_odoo_saas_app_update_dkr_img_wizard kk_odoo_saas.group_saas_manager 1 1 1 1
10 access_saas_app_secret_admin access_saas_app_secret_admin model_kk_odoo_saas_app_ssl_secret kk_odoo_saas.group_saas_manager 1 1 1 1
11 access_saas_app_custom_domain admin_access_saas_app_domain model_saas_app_custom_domain kk_odoo_saas.group_saas_manager 1 1 1 1
12 access_saas_app_custom_domain_portal portal_access_saas_app_domain model_saas_app_custom_domain base.group_portal 1 0 0 0
13 access_kk_odoo_saas_master_db_creds kk_odoo_saas.kk_odoo_saas_master_db_creds model_kk_odoo_saas_k8s_master_db_creds kk_odoo_saas.group_saas_manager 1 1 1 1
14 access_saas_package_admin access_saas_package_admin model_saas_package kk_odoo_saas.group_saas_manager 1 1 1 1
15 access_saas_app_container_argument access_saas_app_container_argument model_saas_app_container_argument kk_odoo_saas.group_saas_manager 1 1 1 1
16 access_saas_app_container_env_var access_saas_app_container_env_var model_saas_app_container_env_var kk_odoo_saas.group_saas_manager 1 1 1 1
17 access_saas_app_manager kk_odoo_saas.kk_odoo_saas model_kk_odoo_saas_app kk_odoo_saas.group_saas_manager 1 1 1 1

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data>
<record model="ir.module.category" id="module_category_saas">
<field name="name">SaaS Management</field>
<field name="sequence">22</field>
</record>
<record id="group_saas_manager" model="res.groups">
<field name="name">SaaS Manager</field>
<field name="category_id" ref="module_category_saas" />
<field
name="users"
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
/>
</record>
<record id="group_saas_client" model="res.groups">
<field name="name">SaaS Client</field>
<field name="category_id" ref="module_category_saas" />
</record>
<!-- manager rules start-->
<!-- manager rules end-->
<!-- customer rules start-->
<!-- only show its own apps to customers-->
<record id="rule_own_saas_apps_only" model="ir.rule">
<field name="name">Personal SaaS App Visibility to Customer</field>
<field ref="model_kk_odoo_saas_app" name="model_id"/>
<field name="domain_force">[('admin_user','=',user.id)]</field>
<field name="groups" eval="[(4, ref('kk_odoo_saas.group_saas_client'))]"/>
</record>
</data>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

10037
kk_odoo_saas/static/src/css/bootstrap.css vendored Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,171 @@
.col-lg-12{
flex: 0 0 100%;
max-width: 100%;
}
#price{
display: inline;
}
.pricing-card-title{
font-size: 32px;
}
.users-qty-change-buttons{
max-width: 30px;
max-height: 30px;
opacity: 0.5;
cursor: pointer;
}
.loader{
position: fixed;
z-index: 99;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
display: flex;
justify-content: center;
align-items: center;
opacity: 0.5;
}
.status{
font-size: 26px;
color: black;
position: fixed;
margin-top: -70px;
}
.loader > img {
width: 100px;
opacity: 0.6;
transform: rotate(-30deg);
}
.transition {
transition: 0.3s;
}
.app, .package{
max-height: 95px;
display: flex;
margin-bottom: 10px;
min-width: 174px;
cursor: pointer;
}
.app-data{
margin-left: 5%;
}
.price-value{
display: inline;
}
.period{
display: inline;
}
.green-border{
border: 2px solid green;
}
.normal-border{
border: 2px solid #FFFFFF;
}
.hid{
display: none;
}
.fnt-24{
font-size: 24px;
}
.leftstr, .rightstr {
float: left;
width: 50%;
}
.rightstr {
text-align: right;
}
.fnt-larger{
font-size: larger;
}
@media (min-width: 1400px) {
#price-window
{
position: fixed;
right: 30%;
top: 20%;
width: 15%;
min-width: 20%;
z-index: 1;
}
.page-alignment{
min-width: 640px;
}
.main-column{
margin-right: auto;
margin-left: 10%;
max-width: 50%;
}
}
@media (min-width: 999px) {
#price-window
{
position: fixed;
right: 15%;
top: 15%;
width: 20%;
z-index: 1;
}
.main-column{
margin-right: auto;
margin-left: 10%;
max-width: 50%;
}
.page-alignment{
min-width: 640px;
}
.app, .package{
margin-left: 7px;
max-width: 31%;
}
}
@media (max-width: 799px) {
#price-window
{
position: fixed;
right: 0;
bottom: -6%;
width: 100%;
z-index: 1;
}
.container{
margin-left: auto !important;
}
.main-column{
margin-right: auto;
margin-left: 10%;
max-width: 80%;
}
}
.col-lg-9{
padding-left: 0px;
}

2338
kk_odoo_saas/static/src/css/font-awesome.css vendored Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,71 @@
html, body {
height: 100%;
}
.o-logs-container{
height: auto;
}
.o-logs{
word-wrap: break-word;
font-family: monospace;
font-size: 13px;
line-height: normal;
margin: 0;
padding-top:80px;
}
.header{
position: fixed;
top: 0;
width: 100%;
background: #f9f9f9;
border-bottom: 1px solid #d3d3d3;
padding: 6px 15px;
box-shadow: 0 0 6px #ddd;
}
.status-bar {
text-align: right;
padding-right: 15px;
}
.button-pause{
position: relative;
color: #555;
font-size: 16px;
padding: 0;
margin-right: 10px;
}
.button-pause i{
margin-left: 9px;
margin-right: 5px;
}
.loader{
position: absolute;
top: -4px;
border: 2px solid #f3f3f3;
border-top: 2px solid #555;
border-radius: 50%;
width: 30px;
height: 30px;
z-index: -1;
}
.loader.loading{
-webkit-animation: spin 1s linear infinite;
animation: spin 1s linear infinite;
}
.form-inline .form-group {
margin-bottom: 0;
vertical-align: middle;
display: inline-block;
}
.form-inline .form-group input {
display: inline-block;
width: auto;
}
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

10364
kk_odoo_saas/static/src/js/jquery.js vendored Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,169 @@
'use strict';
var FETCH_SIZE = 10000;
var FETCH_INTERVAL_TIME = 5000;
var AUTO_PAUSE_TIME = 1000 * 60 * 5;
function fetch_logs(begin, end) {
return $.ajax({
dataType: "text",
cache: false,
headers: {Range: 'bytes=' + (begin === false ? '' : begin) + '-' + (end === false ? '' : end)},
}).then(function (data, s, xhr) {
data = data.replace(/^\n/, "");
data = data.replace(/\n$/, "");
var content_range = xhr.getResponseHeader("Content-Range");
var bytes = content_range ? /bytes ([0-9]*)-([0-9]*)\/([0-9]*)/.exec(content_range) : undefined;
var begin = bytes ? parseInt(bytes[1]) : 0;
var end = bytes ? parseInt(bytes[2]) : data.length;
var size = bytes ? parseInt(bytes[3]) : data.length + 1;
return {
data: data,
begin: begin,
end: end,
size: size,
};
});
}
$(document).ready(function () {
var fetch_interval;
var min;
var max;
var auto_scroll = true;
var def_top;
var def_bottom;
function init() {
$('.o-logs span').remove();
return fetch_logs(false, 1).then(function (result) {
min = max = Math.max(0, result.size - FETCH_SIZE);
def_bottom = def_top = undefined;
});
}
// borrowed from https://github.com/janl/mustache.js/blob/master/mustache.js
function _escapeHTML(string) {
var entityMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
};
return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap(s) {
return entityMap[s];
});
}
function _prepare_line(line) {
var result = '<span class="o-log-line">'
line.split('\n').forEach(function (l) {
result += '<span style="white-space: pre;">' + _escapeHTML(l) + '</span><br/>';
});
result += '</span>'
result = $(result);
filter(result);
return result;
}
function append_logs() {
if (!def_bottom || def_bottom.state() !== 'pending') {
def_bottom = fetch_logs(max, false).then(function (result) {
if (max !== result.end) {
max = result.end;
var splits = result.data.split(/\s+(?=[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3})/)
splits.forEach(function (line) {
$('.o-logs').append(_prepare_line(line));
});
if (auto_scroll) {
window.scrollTo(0, document.body.scrollHeight);
}
}
}).fail(function (xhr) {
if (xhr.status === 416) {
return init();
}
});
}
return def_bottom;
}
function prepend_logs() {
if (min > 0 && (!def_top || def_top.state() !== 'pending')) {
def_top = fetch_logs(Math.max(0, min - FETCH_SIZE), min).then(function (result) {
min = result.begin;
var lines = result.data.split('\n');
var first_line = lines.pop();
if (first_line) {
$('.o-logs span').first().prepend(_escapeHTML(first_line));
}
lines.reverse();
lines.forEach(function (line) {
$('.o-logs').prepend(_prepare_line(line));
});
window.scrollTo(0, 20);
}).fail(function (xhr) {
if (xhr.status === 416) {
return init();
}
});
}
return def_top;
}
function toggle_pause() {
$('i').toggle();
$('.loader').toggleClass('loading');
if (fetch_interval) {
clearInterval(fetch_interval);
fetch_interval = undefined;
} else {
fetch_interval = setInterval(append_logs, FETCH_INTERVAL_TIME);
setTimeout(toggle_pause, AUTO_PAUSE_TIME);
}
}
function filter(elements) {
var filter = $('#filter').val();
elements.filter(':contains(' + filter + ')').show();
elements.filter(':not(:contains(' + filter + '))').hide();
}
$(window).scroll(function () {
if ($(window).scrollTop() + $(window).height() === $(document).height()) {
auto_scroll = true;
} else {
auto_scroll = false;
}
if ($(window).scrollTop() === 0) {
prepend_logs();
}
});
function fill_page() {
if (!def_bottom) {
append_logs().then(fill_page);
} else if ($(window).height() === $(document).height() && min != 0 && (!def_top || def_top.state() !== 'pending')) {
prepend_logs().then(fill_page);
} else {
$(window).scrollTop($(document).height());
}
}
init().then(function () {
fill_page();
toggle_pause();
});
$('.button-pause').click(toggle_pause);
$('#filter').on('input', function () {
filter($('.o-logs .o-log-line'));
});
$('#filter').keypress(function (event) {
if (event.keyCode === 10 || event.keyCode === 13) {
event.preventDefault();
}
});
});

View File

@ -0,0 +1,29 @@
odoo.define('saas_apps.filter_button', function (require) {
"use strict";
var core = require('web.core');
var session = require('web.session');
var ListController = require('web.ListController');
ListController.include({
renderButtons: function($node) {
this._super.apply(this, arguments);
if (this.$buttons) {
var refresh_apps_button = this.$buttons.find('.refresh_apps_button');
if (refresh_apps_button.length) {
refresh_apps_button.on("click", this.proxy('refresh_apps_button'));
}
}
},
refresh_apps_button: function () {
// Loading all modules in saas.line from ir.module.module
this._rpc({
"model": "saas.app",
"method": "action_make_applist_from_local_instance",
"args": [],
}).then(function (result) {
window.location.reload()
});
}
});
});

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<template xml:space="preserve">
<t t-extend="ListView.buttons">
<t t-jquery="button.o_list_button_add" t-operation="after">
<button t-if="widget.modelName == 'saas.app'" type="button" class="btn btn-primary refresh_apps_button" accesskey="f">
Refresh
</button>
</t>
</t>
</template>

1
kk_odoo_saas/utils/__init__.py Executable file
View File

@ -0,0 +1 @@
from . import k8s_deployment

View File

@ -0,0 +1,52 @@
from kubernetes import config, client
from kubernetes.stream import stream
from odoo.addons.smile_log.tools import SmileDBLogger
from odoo.exceptions import UserError
import yaml
# import git_aggregator
def del_git_dir(self, path):
"""
It will delete addons directory inside running container
"""
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
if self.app_name and path:
try:
data2 = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data2)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
core_v1_api = client.CoreV1Api()
try:
pod = core_v1_api.list_namespaced_pod(namespace='default', label_selector='app={}'.format(self.app_name))
except Exception as e:
raise UserError("Unable to connect to cluster")
resp1 = stream(core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=['chmod', '-R', 'ugo+rw', path],
stderr=True, stdin=False,
stdout=True, tty=False)
resp = stream(core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=['rm', '-rf', path ],
stderr=True, stdin=False,
stdout=True, tty=False)
resp3 = stream(core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=['mkdir', path ],
stderr=True, stdin=False,
stdout=True, tty=False)
_logger.info(str(resp1))
_logger.info(str(resp))
_logger.info(str(resp3))
_logger.info(str(path))
_logger.info(str("code deleted"))

190
kk_odoo_saas/utils/deployment.py Executable file
View File

@ -0,0 +1,190 @@
# -*- coding: utf-8 -*-
import logging
from kubernetes import client
from odoo.addons.smile_log.tools import SmileDBLogger
from odoo.exceptions import UserError, ValidationError
_logger = logging.getLogger(__name__)
def create_deployment(meta_data, specs, namespace="default", self=False):
# Deployment
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
deployment = client.V1Deployment(
api_version="apps/v1",
kind="Deployment",
metadata=meta_data,
spec=specs)
k8s_apps_v1 = client.AppsV1Api()
try:
resp = k8s_apps_v1.create_namespaced_deployment(
body=deployment,
namespace=namespace,
)
_logger.info("Deployment created. name='%s'" % resp.metadata.name)
except client.exceptions.ApiException as e:
_logger.error(str(e))
def create_docker_repo_secret(app_name, namespace="default", self=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
k8s_apps_v1 = client.CoreV1Api()
secret = client.V1Secret(
metadata=client.V1ObjectMeta(
name=app_name+'-dkr-registry-key',
labels={
"app": app_name,
"tier": "backend"
}
),
data={
'.dockerconfigjson': self.docker_image.b64_dkr_config
},
type='kubernetes.io/dockerconfigjson',
)
try:
resp = k8s_apps_v1.create_namespaced_secret(
body=secret, namespace=namespace)
_logger.info("Secret created. name='%s'" % resp.metadata.name)
return True
except client.exceptions.ApiException as e:
_logger.error(str(e))
return False
def delete_docker_repo_secret(app_name, namespace="default", self=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
k8s_apps_v1 = client.CoreV1Api()
try:
resp = k8s_apps_v1.delete_namespaced_secret(app_name+'-dkr-registry-key', namespace=namespace)
_logger.info(str(resp))
return True
except client.exceptions.ApiException as e:
_logger.error(str(e))
return False
def create_odoo_deployment(app_name, namespace="default", self=False):
image = 'odoo:15.0'
res_limits = {'ephemeral-storage': '1Gi'}
image_pull_secrets = []
if self.is_custom_image and self.docker_image:
image = f'{self.docker_image.name}:{self.docker_image.tag}'
if self.docker_image.is_pvt_dkr_repo and self.docker_image.b64_dkr_config:
if create_docker_repo_secret(app_name, namespace, self):
sec_name = f'{app_name}-dkr-registry-key'
image_pull_secrets.append(client.V1LocalObjectReference(name=sec_name))
meta_data = client.V1ObjectMeta(name=f'{app_name}-odoo-deployment', labels={'app': app_name})
args_odoo = [
f'--database={self.sub_domain_name}',
# f'--workers=3',
# f'--max-cron-threads=2',
# f'--http-port=8069',
# f'{self.docker_image.gevent_key}=8072',
]
if self.demo_data:
args_odoo.append('--without-demo=False')
else:
args_odoo.append('--without-demo=True')
if self.module_ids:
module_names = ','.join(self.module_ids.mapped('name'))
# module_names = ''
# for module in self.module_ids:
# module_names = module_names + module.name + ','
args_odoo.append(f'--init={module_names}')
# TODO ??? why ??? ==================
# if self.db_server_id:
# _logger.critical('Cant deploy app, PG username or password cant find')
# UserError("Cant deploy app, PG username or password cant find")
limits = client.V1ResourceRequirements(limits=res_limits)
tolerations = []
node_selector = {}
if self and self.is_dedicated_node and self.node_id:
# tolerations = [client.V1Toleration(effect='NoSchedule', key=self.node_key, value=self.node_value, operator='Equal')]
# specific for aws clusters
node_selector['kubernetes.io/hostname'] = self.node_id.name
odoo_container = client.V1Container(
name='odoo',
image=image,
env=[
client.V1EnvVar(name="HOST", value=self.db_server_id.server_url),
client.V1EnvVar(name="USER", value=self.db_server_id.master_username),
client.V1EnvVar(name="PASSWORD", value=self.db_server_id.master_pass),
client.V1EnvVar(name="PORT", value=self.db_server_id.server_port),
client.V1EnvVar(name="ODOO_HTTP_SOCKET_TIMEOUT", value="100"),
],
ports=[
client.V1ContainerPort(container_port=8069, name="odoo-port"),
client.V1ContainerPort(container_port=8072, name="longpolling"),
],
args=args_odoo,
image_pull_policy='Always',
# command=['chown', '-R', '101:101', '/mnt/extra-addons'],
resources=limits,
# comment following line, if you want to run as odoo user
# security_context=client.V1SecurityContext(run_as_user=0, run_as_group=0),
volume_mounts=[
client.V1VolumeMount(name=f'{app_name}-odoo-web-pv-storage', mount_path='/var/lib/odoo/')
]
)
# pod Volume Claim
volume_claim = client.V1PersistentVolumeClaimVolumeSource(claim_name=f'{app_name}-odoo-web-pv-claim')
# pod volume
volume = client.V1Volume(name=f'{app_name}-odoo-web-pv-storage', persistent_volume_claim=volume_claim)
# Strategy
strategy = client.V1DeploymentStrategy(type='Recreate')
# Template
# for running as an odoo user changes instead of stash
spec = client.V1PodSpec(
containers=[odoo_container],
volumes=[volume],
image_pull_secrets=image_pull_secrets,
security_context=client.V1PodSecurityContext(
run_as_group=101,
run_as_user=101,
fs_group=101,
fs_group_change_policy='Always'
),
node_selector=node_selector,
)
template = client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(labels={'app': app_name, 'tier': "backend"}),
spec=spec
)
selector = client.V1LabelSelector(match_labels={'app': app_name, 'tier': 'backend'})
# Spec
specs = client.V1DeploymentSpec(
replicas=1,
strategy=strategy,
selector=selector,
template=template,
)
create_deployment(meta_data, specs, namespace, self=self)
def delete_odoo_deployment(app_name, namespace="default", self=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
dep_name = app_name + "-odoo-deployment"
core_v1_api = client.AppsV1Api()
try:
deployment = core_v1_api.delete_namespaced_deployment(name=dep_name, namespace=namespace)
if self.is_custom_image and self.docker_image:
if self.docker_image.is_pvt_dkr_repo and self.docker_image.b64_dkr_config:
delete_docker_repo_secret(app_name, namespace, self)
_logger.info(str(deployment))
except client.exceptions.ApiException as e:
_logger.error(str(e))

299
kk_odoo_saas/utils/ingress.py Executable file
View File

@ -0,0 +1,299 @@
# -*- coding: utf-8 -*-
import logging
from kubernetes import client, config
from odoo.exceptions import ValidationError
from odoo.addons.smile_log.tools import SmileDBLogger
def create_ingress(app_name, self=False):
_logger = logging.getLogger(__name__)
if self:
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
# create_ingress
if not self.domain_name and self.sub_domain_name:
return ValidationError('Either Domain name or Subdomain name is not Valid')
else:
host = f'{self.sub_domain_name}.{self.domain_name}'
networking_v1_api = client.NetworkingV1Api()
rules = [
client.V1IngressRule(
host=host,
http=client.V1HTTPIngressRuleValue(
paths=[
client.V1HTTPIngressPath(
path='/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=80,
),
name=f'{app_name}-odoo-service',
)
)
),
client.V1HTTPIngressPath(
path='/longpolling/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=8072,
),
name=f'{app_name}-odoo-service',
)
)
),
]
)
)
]
odoo_urls = [host]
if self and self.custom_domain_ids:
for custom_domain in self.custom_domain_ids:
rules.append(
client.V1IngressRule(
host=custom_domain.name,
http=client.V1HTTPIngressRuleValue(
paths=[
client.V1HTTPIngressPath(
path='/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=80,
),
name=f'{app_name}-odoo-service',
)
)
),
client.V1HTTPIngressPath(
path='/longpolling/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=8072,
),
name=f'{app_name}-odoo-service',
)
)
)
]
)
)
)
odoo_urls.append(custom_domain.name)
body = client.V1Ingress(
kind='Ingress',
metadata=client.V1ObjectMeta(
name=f'{app_name}-ingress',
labels={'app': app_name},
annotations={
'kubernetes.io/ingress.class': 'nginx',
'cert-manager.io/cluster-issuer': 'letsencrypt-prod',
},
),
spec=client.V1IngressSpec(
rules=rules,
tls=[
client.V1IngressTLS(
hosts=odoo_urls,
secret_name=f'{self.app_name}tls',
)
]
)
)
try:
networking_v1_api.create_namespaced_ingress(
namespace='default',
body=body
)
except client.exceptions.ApiException as e:
_logger.error(str(e))
def delete_odoo_ingress(app_name, namespace="default", self=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
networking_v1_api = client.NetworkingV1Api()
ing_name = f'{app_name}-ingress'
try:
ing = networking_v1_api.delete_namespaced_ingress(name=ing_name, namespace=namespace)
_logger.info(str(ing))
except client.exceptions.ApiException as e:
_logger.error(str(e))
def update_odoo_ingress(app_name, namespace="default", self=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
networking_v1_api = client.NetworkingV1Api()
ing_name = f'{app_name}-ingress'
if not self.domain_name and self.sub_domain_name:
return ValidationError('Either Domain name or Subdomain name is not Valid')
else:
host = f'{self.sub_domain_name}.{self.domain_name}'
rules = [
client.V1IngressRule(
host=host,
http=client.V1HTTPIngressRuleValue(
paths=[
client.V1HTTPIngressPath(
path='/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=80,
),
name=f'{app_name}-odoo-service',
)
)
),
client.V1HTTPIngressPath(
path='/longpolling/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=8072,
),
name=f'{app_name}-odoo-service',
)
)
)
]
)
)
]
tls_hosts = [host]
if self and self.custom_domain_ids:
for custom_domain in self.custom_domain_ids:
rules.append(
client.V1IngressRule(
host=custom_domain.name,
http=client.V1HTTPIngressRuleValue(
paths=[
client.V1HTTPIngressPath(
path='/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=80,
),
name=f'{app_name}-odoo-service',
)
)
),
client.V1HTTPIngressPath(
path='/longpolling/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=8072,
),
name=f'{app_name}-odoo-service',
)
)
)
]
)
)
)
tls_hosts.append(custom_domain.name)
body = client.V1Ingress(
kind='Ingress',
metadata=client.V1ObjectMeta(
name=f'{app_name}-ingress',
labels={"app": app_name},
annotations={
'kubernetes.io/ingress.class': 'nginx',
}
),
spec=client.V1IngressSpec(
rules=rules,
tls=[
client.V1IngressTLS(
hosts=tls_hosts,
secret_name=f'{self.app_name}tls'
)
]
)
)
try:
ing = networking_v1_api.patch_namespaced_ingress(name=ing_name, namespace=namespace, body=body)
_logger.info(str(ing))
except client.exceptions.ApiException as e:
_logger.error(str(e))
# ======================================================================================================================
def create_ingress_http(app_name, self=False):
_logger = logging.getLogger(__name__)
if self:
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
# create_ingress
if not self.domain_name and self.sub_domain_name:
return ValidationError('Either Domain name or Subdomain name is not Valid')
networking_v1_api = client.NetworkingV1Api()
host = f'{self.sub_domain_name}.{self.domain_name}'
rules = [
client.V1IngressRule(
host=host,
http=client.V1HTTPIngressRuleValue(
paths=[
client.V1HTTPIngressPath(
path='/',
path_type='Prefix',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(number=8069),
name=f'{app_name}-odoo-service',
)
)
)
]
)
)
]
body = client.V1Ingress(
kind='Ingress',
metadata=client.V1ObjectMeta(
name=f'{app_name}-ingress',
labels={'app': app_name},
annotations={
'kubernetes.io/ingress.class': 'nginx',
'cert-manager.io/cluster-issuer': 'letsencrypt-prod',
},
),
spec=client.V1IngressSpec(
rules=rules,
)
)
try:
networking_v1_api.create_namespaced_ingress(namespace='default', body=body)
except client.exceptions.ApiException as e:
_logger.error(str(e))

View File

@ -0,0 +1,271 @@
# -*- coding: utf-8 -*-
from kubernetes import config, client
from kubernetes.stream import stream
import yaml
from odoo.exceptions import UserError
from odoo.addons.smile_log.tools import SmileDBLogger
from .pg_server import delete_databases
from .utils import generate_commit_sha
from .odoo_components import (
deploy_odoo_components,
delete_odoo_components,
delete_odoo_components_from_options,
update_odoo_components
)
def create_deployment(app_name, config_file, self=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
"""
Configs can be set in Configuration class directly or using helper
utility. If no argument provided, the config will be loaded from
default location.
"""
try:
data2 = yaml.safe_load(config_file)
config.load_kube_config_from_dict(data2)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
if app_name:
deploy_odoo_components(app_name=app_name, namespace="default", self=self)
else:
_logger.error("Cant find App Name")
raise UserError("Cant find App Name")
def delete_app_with_options(self, delete_db, delete_pv, delete_svc, delete_ing, delete_deployment):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
data = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
if self.app_name:
delete_odoo_components_from_options(
app_name=self.app_name,
namespace="default",
self=self,
delete_db=delete_db,
delete_pv=delete_pv,
delete_svc=delete_svc,
delete_ing=delete_ing,
delete_deployment=delete_deployment
)
if delete_db:
delete_databases(self)
else:
_logger.error("Cant find App Name")
raise UserError("Cant find App Name")
def update_app(self):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
data = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
if self.app_name:
update_odoo_components(app_name=self.app_name, namespace="default", self=self)
else:
_logger.error("Cant find App Name")
raise UserError("Cant find App Name")
def fetch_secrets_from_cluster(self):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
data2 = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data2)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
secs = []
if self.app_name:
core_v1_api = client.CoreV1Api()
secrs = core_v1_api.list_namespaced_secret(namespace='default')
for sec in secrs.items:
secs.append(sec.metadata.name)
return secs
def deploy_apps_from_git(self):
"""
To pull code from github inside running container
"""
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
data2 = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data2)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
if self.app_name:
core_v1_api = client.CoreV1Api()
pod = core_v1_api.list_namespaced_pod(namespace='default', label_selector='app={}'.format(self.app_name))
if self.is_extra_addon and self.extra_addons and pod and pod.items:
base_version = self.docker_image.base_version
clone_path = "/var/lib/odoo/addons/" + str(base_version)
if self.is_private_repo and self.git_token:
url = self.extra_addons
url = url.replace("http://", "")
url = url.replace("https://", "")
url = url.replace("www.", "")
git_url = "https://oauth2:{0}@{1}".format(self.git_token, url)
else:
git_url = self.extra_addons
is_clone_error = False
error = ''
exec_command = ['git', '-C', clone_path, 'pull']
resp = stream(core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=exec_command,
stderr=True, stdin=True,
stdout=True, tty=False,
_preload_content=False)
while resp.is_open():
resp.update(timeout=10)
if resp.peek_stdout():
_logger.info(str(resp.read_stdout()))
if resp.peek_stderr():
is_clone_error = True
error = resp.read_stderr()
_logger.error(str(error))
break
resp.close()
if is_clone_error:
if error and "not a git repository (or any" in error:
resp1 = stream(core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=['chmod', '-R', 'ugo+rw', clone_path],
stderr=True, stdin=False,
stdout=True, tty=False,
_preload_content=False)
resp = stream(core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=['git', 'clone', git_url, clone_path],
stderr=True, stdin=False,
stdout=True, tty=False,
_preload_content=False)
while resp.is_open():
resp.update(timeout=25)
if resp.peek_stdout():
_logger.info(str(resp.read_stdout()))
if resp.peek_stderr():
error = resp.read_stderr()
_logger.error(str(error))
else:
_logger.info(str(
"No Response"
))
resp.close()
else:
return False
def restart_odoo_service(self):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
data2 = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data2)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
if self.app_name:
core_v1_api = client.CoreV1Api()
pod = core_v1_api.list_namespaced_pod(
namespace='default',
label_selector=f'app={self.app_name}')
exec_command = ['./mnt/restart_odoo.sh']
resp = stream(
core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=exec_command,
stderr=True,
stdin=True,
stdout=True,
tty=False,
# _preload_content=False
)
while resp.is_open():
resp.update(timeout=10)
if resp.peek_stdout():
_logger.info(str(resp.read_stdout()))
if resp.peek_stderr():
error = resp.read_stderr()
_logger.error(str(error))
break
resp.close()
def read_deployment(self, dep_type='odoo'):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
data = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
if self.app_name:
dep_name = self.app_name + "-odoo-deployment"
core_v1_api = client.AppsV1Api()
try:
deployment = core_v1_api.read_namespaced_deployment(name=dep_name, namespace='default')
if deployment:
return deployment
return
except Exception as e:
pass
else:
_logger.error("Cant find App Name")
raise UserError("Cant find App Name")
def update_deployment(self, container_arguments, dep_type='odoo', env_vars=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
data = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
if self.app_name:
core_v1_api = client.AppsV1Api()
try:
deployment = read_deployment(self=self)
if container_arguments:
deployment.spec.template.spec.containers[0].args = eval(container_arguments)
if env_vars:
deployment.spec.template.spec.containers[0].env = env_vars
deployment.spec.template.metadata.labels['COMMIT_SHA'] = generate_commit_sha(10)
patched_deployment = core_v1_api.patch_namespaced_deployment(name=deployment.metadata.name,
namespace='default',
body=deployment)
return patched_deployment
except Exception as e:
_logger.error(str(e))
else:
_logger.error("Cant find App Name")
raise UserError("Cant find App Name")

34
kk_odoo_saas/utils/logs.py Executable file
View File

@ -0,0 +1,34 @@
from kubernetes import config, client
import yaml
from kubernetes.client.rest import ApiException
from odoo.exceptions import UserError
import logging
_logger = logging.getLogger(__name__)
def read_logs(app_name, self=False, config_file=None, since_seconds=None, previous=False, tail_lines=None):
# Configs can be set in Configuration class directly or using helper
# utility. If no argument provided, the config will be loaded from
# default location.
try:
data2 = yaml.safe_load(config_file)
config.load_kube_config_from_dict(data2)
except config.config_exception.ConfigException as e:
raise UserError("Unable to Connect K8s Cluster")
try:
api_instance = client.CoreV1Api()
odoo_pods = api_instance.list_namespaced_pod(namespace='default',
label_selector='app={0},tier={1}'.format(str(self.app_name),
'backend'))
for pod in odoo_pods.items:
if pod.metadata and pod.metadata.name and (self.app_name + '-odoo-deployment' in pod.metadata.name):
odoo_logs = api_instance.read_namespaced_pod_log(name=pod.metadata.name, namespace='default',
tail_lines=tail_lines, since_seconds=since_seconds)
return odoo_logs
return False
except ApiException as e:
_logger.error(e)
return False

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from .service import create_odoo_service, delete_odoo_service
from .deployment import create_odoo_deployment, delete_odoo_deployment
from .pv_claim import create_odoo_pv_claim, delete_odoo_pv_claim
from .ingress import delete_odoo_ingress, update_odoo_ingress
from .utils import delete_job_task
def deploy_odoo_components(app_name, namespace, self=False):
create_odoo_pv_claim(app_name, namespace, self=self)
create_odoo_service(app_name, namespace, self=self)
create_odoo_deployment(app_name, namespace, self=self)
def delete_odoo_components(app_name, namespace, self=False):
delete_odoo_pv_claim(app_name, namespace, self=self)
delete_odoo_service(app_name, namespace, self=self)
delete_odoo_deployment(app_name, namespace, self=self)
delete_odoo_ingress(app_name, namespace, self=self)
delete_job_task(self)
def delete_odoo_components_from_options(
app_name, namespace, self=False, delete_db=False, delete_pv=False,
delete_svc=False, delete_ing=False, delete_deployment=False):
if delete_pv:
delete_odoo_pv_claim(app_name, namespace, self=self)
if delete_svc:
delete_odoo_service(app_name, namespace, self=self)
if delete_deployment:
delete_odoo_deployment(app_name, namespace, self=self)
if delete_ing:
delete_odoo_ingress(app_name, namespace, self=self)
delete_job_task(self)
def update_odoo_components(app_name, namespace, self=False):
update_odoo_ingress(app_name, namespace, self)

68
kk_odoo_saas/utils/pg_query.py Executable file
View File

@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
import logging
import psycopg2
import sys
_logger = logging.getLogger(__name__)
class PgQuery(object):
"""
USAGE:
postgresX = ['localhost', 'sadsadsad', 'admin', 'codetuple']
pgX = TaskMigration(*postgresX)
with pgX:
result = pgX.selectQuery(query)
"""
def __init__(self, host, database, user, password, port=5432):
self.host = host
self.database = database
self.user = user
self.password = password
self.dbConnection = False
self.cursor = False
self.port = port
def __enter__(self):
try:
self.dbConnection = psycopg2.connect(
host=self.host,
database=self.database,
user=self.user,
password=self.password,
port=self.port,
)
self.cursor = self.dbConnection.cursor()
except Exception as e:
_logger.info("Error in Postgres Connection: %r" % e)
sys.exit()
return self.dbConnection
def __exit__(self, exc_type, exc_val, exc_tb):
if self.dbConnection:
# self.dbConnection.close()
pass
def select_query(self, queryString):
status = True
try:
self.cursor.execute(queryString)
except Exception as e:
status = False
return status
else:
return self.cursor.fetchall()
def execute_query(self, queryString):
status = True
try:
self.cursor.execute(queryString)
self.dbConnection.commit()
except Exception as e:
_logger.info(queryString)
_logger.info(e)
status = False
finally:
return status

55
kk_odoo_saas/utils/pg_server.py Executable file
View File

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
import logging
from contextlib import closing
from psycopg2 import sql, connect
import odoo
from odoo.exceptions import AccessError, UserError
from .utils import generate_temp_password
_logger = logging.getLogger(__name__)
def drop_db(self, db_name):
if self:
child_conn = self.get_pg_db_connection(db=db_name)
child_conn.set_session(autocommit=True)
with closing(child_conn.cursor()) as cr:
odoo.service.db._drop_conn(cr, db_name)
try:
cr.execute(sql.SQL('DROP DATABASE {}').format(sql.Identifier(db_name)))
except Exception as e:
_logger.info('DROP DB: %s failed:\n%s', db_name, e)
child_conn.close()
raise UserError("Couldn't drop database %s: %s" % (db_name, e))
else:
child_conn.close()
_logger.info('DROP DB: %s', db_name)
return True
def delete_databases(self):
if self and self.client_db_name:
# dbs = get_databases(self)
drop_db(self, self.client_db_name)
def get_admin_credentials(self):
if self and self.client_db_name:
# FOR admin user_id = 2
child_conn = self.get_pg_db_connection(db=self.client_db_name)
query = sql.SQL("SELECT login, COALESCE(password, '') FROM res_users WHERE id=2;")
with closing(child_conn.cursor()) as cr:
try:
cr.execute(query)
res = cr.fetchall()
child_conn.close()
except Exception:
_logger.exception('Getting Credentials failed')
res = False
child_conn.close()
return res, self.client_db_name
return False, False

55
kk_odoo_saas/utils/pv_claim.py Executable file
View File

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
from kubernetes import client, config
from odoo.addons.smile_log.tools import SmileDBLogger
def create_pv_claim(meta_data, specs, namespace='default', self=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
k8s_apps_v1 = client.CoreV1Api()
dep = client.V1PersistentVolumeClaim(
api_version='v1',
kind='PersistentVolumeClaim',
metadata=meta_data,
spec=specs
)
try:
resp = k8s_apps_v1.create_namespaced_persistent_volume_claim(body=dep, namespace=namespace)
_logger.info(f'Volume created. status={resp.metadata.name}')
except client.exceptions.ApiException as e:
_logger.error(msg=str(e))
def create_odoo_pv_claim(app_name, namespace="default", self=False):
specs = client.V1PersistentVolumeClaimSpec(
access_modes=[
'ReadWriteOnce',
],
storage_class_name="gp2",
resources=client.V1ResourceRequirements(
requests={
'storage': '1Gi', # TODO: setup in app
}
)
)
meta_data = client.V1ObjectMeta(
name=F'{app_name}-odoo-web-pv-claim',
labels={'app': app_name}
)
create_pv_claim(meta_data=meta_data, specs=specs, namespace=namespace, self=self)
def delete_odoo_pv_claim(app_name, namespace="default", self=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
claim_name = f'{app_name}-odoo-web-pv-claim'
core_v1_api = client.CoreV1Api()
try:
pv = core_v1_api.delete_namespaced_persistent_volume_claim(name=claim_name, namespace=namespace)
_logger.info(str(pv))
except client.exceptions.ApiException as e:
_logger.error(str(e))

65
kk_odoo_saas/utils/service.py Executable file
View File

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
import logging
from kubernetes import client
from odoo.addons.smile_log.tools import SmileDBLogger
_logger = logging.getLogger(__name__)
def create_service(specs, metadata, namespace='default', self=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
core_v1_api = client.CoreV1Api()
body = client.V1Service(
api_version='v1',
kind='Service',
metadata=metadata,
spec=specs
)
# Creation of the Deployment in specified namespace
try:
service = core_v1_api.create_namespaced_service(namespace=namespace, body=body)
_logger.info(f'Service created. status={service.metadata.name}')
except client.exceptions.ApiException as e:
_logger.error(str(e))
def create_odoo_service(app_name, namespace, self=False):
service_name = f'{app_name}-odoo-service'
specs = client.V1ServiceSpec(
selector={
'app': app_name,
'tier': 'backend',
},
ports=[
client.V1ServicePort(
name='odoo-port',
protocol='TCP',
port=8069,
target_port=8069,
)
],
type='NodePort'
)
metadata = client.V1ObjectMeta(
name=service_name,
labels={'app': app_name}
)
create_service(metadata=metadata, specs=specs, namespace=namespace, self=self)
def delete_odoo_service(app_name, namespace, self=False):
service_name = f'{app_name}-odoo-service'
core_v1_api = client.CoreV1Api()
try:
service = core_v1_api.delete_namespaced_service(name=service_name, namespace=namespace)
_logger.info(service)
except client.exceptions.ApiException as e:
_logger.error(str(e))

34
kk_odoo_saas/utils/utils.py Executable file
View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
def generate_temp_password(length):
if not isinstance(length, int) or length < 8:
raise ValueError("temp password must have positive length")
chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*()"
from os import urandom
# Python 3 (urandom returns bytes)
return "".join(chars[c % len(chars)] for c in urandom(length))
def generate_commit_sha(length):
if not isinstance(length, int) or length < 8:
raise ValueError("sha must have positive length")
chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
from os import urandom
# Python 3 (urandom returns bytes)
return "".join(chars[c % len(chars)] for c in urandom(length))
def delete_job_task(self):
if self and self.id:
job_q_env = self.env['queue.job']
jobs = job_q_env.search([
"|", "|", "|",
("state", "=", "pending"),
("state", "=", "enqueued"),
("state", "=", "started"),
("state", "=", "failed"),
('func_string', '=', "kk_odoo_saas.app({0},).post_init_tasks()".format(self.id))])
for job in jobs:
job.button_done()

View File

@ -0,0 +1 @@
from . import k8s_deployment

View File

@ -0,0 +1,52 @@
from kubernetes import config, client
from kubernetes.stream import stream
from odoo.addons.smile_log.tools import SmileDBLogger
from odoo.exceptions import UserError
import yaml
# import git_aggregator
def del_git_dir(self, path):
"""
It will delete addons directory inside running container
"""
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
if self.app_name and path:
try:
data2 = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data2)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
core_v1_api = client.CoreV1Api()
try:
pod = core_v1_api.list_namespaced_pod(namespace='default', label_selector='app={}'.format(self.app_name))
except Exception as e:
raise UserError("Unable to connect to cluster")
resp1 = stream(core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=['chmod', '-R', 'ugo+rw', path],
stderr=True, stdin=False,
stdout=True, tty=False)
resp = stream(core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=['rm', '-rf', path ],
stderr=True, stdin=False,
stdout=True, tty=False)
resp3 = stream(core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=['mkdir', path ],
stderr=True, stdin=False,
stdout=True, tty=False)
_logger.info(str(resp1))
_logger.info(str(resp))
_logger.info(str(resp3))
_logger.info(str(path))
_logger.info(str("code deleted"))

View File

@ -0,0 +1,190 @@
# -*- coding: utf-8 -*-
import logging
from kubernetes import client
from odoo.addons.smile_log.tools import SmileDBLogger
from odoo.exceptions import UserError, ValidationError
_logger = logging.getLogger(__name__)
def create_deployment(meta_data, specs, namespace="default", self=False):
# Deployment
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
deployment = client.V1Deployment(
api_version="apps/v1",
kind="Deployment",
metadata=meta_data,
spec=specs)
k8s_apps_v1 = client.AppsV1Api()
try:
resp = k8s_apps_v1.create_namespaced_deployment(
body=deployment,
namespace=namespace,
)
_logger.info("Deployment created. name='%s'" % resp.metadata.name)
except client.exceptions.ApiException as e:
_logger.error(str(e))
def create_docker_repo_secret(app_name, namespace="default", self=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
k8s_apps_v1 = client.CoreV1Api()
secret = client.V1Secret(
metadata=client.V1ObjectMeta(
name=app_name+'-dkr-registry-key',
labels={
"app": app_name,
"tier": "backend"
}
),
data={
'.dockerconfigjson': self.docker_image.b64_dkr_config
},
type='kubernetes.io/dockerconfigjson',
)
try:
resp = k8s_apps_v1.create_namespaced_secret(
body=secret, namespace=namespace)
_logger.info("Secret created. name='%s'" % resp.metadata.name)
return True
except client.exceptions.ApiException as e:
_logger.error(str(e))
return False
def delete_docker_repo_secret(app_name, namespace="default", self=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
k8s_apps_v1 = client.CoreV1Api()
try:
resp = k8s_apps_v1.delete_namespaced_secret(app_name+'-dkr-registry-key', namespace=namespace)
_logger.info(str(resp))
return True
except client.exceptions.ApiException as e:
_logger.error(str(e))
return False
def create_odoo_deployment(app_name, namespace="default", self=False):
image = 'odoo:15.0'
res_limits = {'ephemeral-storage': '1Gi'}
image_pull_secrets = []
if self.is_custom_image and self.docker_image:
image = f'{self.docker_image.name}:{self.docker_image.tag}'
if self.docker_image.is_pvt_dkr_repo and self.docker_image.b64_dkr_config:
if create_docker_repo_secret(app_name, namespace, self):
sec_name = f'{app_name}-dkr-registry-key'
image_pull_secrets.append(client.V1LocalObjectReference(name=sec_name))
meta_data = client.V1ObjectMeta(name=f'{app_name}-odoo-deployment', labels={'app': app_name})
args_odoo = [
f'--database={self.sub_domain_name}',
f'--workers=3',
f'--max-cron-threads=2',
f'--http-port=8069',
f'{self.docker_image.gevent_key}=8072'
]
if self.demo_data:
args_odoo.append('--without-demo=False')
else:
args_odoo.append('--without-demo=True')
if self.module_ids:
module_names = ','.join(self.module_ids.mapped('name'))
# module_names = ''
# for module in self.module_ids:
# module_names = module_names + module.name + ','
args_odoo.append(f'--init={module_names}')
# TODO ??? why ??? ==================
# if self.db_server_id:
# _logger.critical('Cant deploy app, PG username or password cant find')
# UserError("Cant deploy app, PG username or password cant find")
limits = client.V1ResourceRequirements(limits=res_limits)
tolerations = []
node_selector = {}
if self and self.is_dedicated_node and self.node_id:
# tolerations = [client.V1Toleration(effect='NoSchedule', key=self.node_key, value=self.node_value, operator='Equal')]
# specific for aws clusters
node_selector['kubernetes.io/hostname'] = self.node_id.name
odoo_container = client.V1Container(
name='odoo',
image=image,
env=[
client.V1EnvVar(name="HOST", value=self.db_server_id.server_url),
client.V1EnvVar(name="USER", value=self.db_server_id.master_username),
client.V1EnvVar(name="PASSWORD", value=self.db_server_id.master_pass),
client.V1EnvVar(name="PORT", value=self.db_server_id.server_port),
client.V1EnvVar(name="ODOO_HTTP_SOCKET_TIMEOUT", value="100"),
],
ports=[
client.V1ContainerPort(container_port=8069, name="odoo-port"),
client.V1ContainerPort(container_port=8072, name="longpolling"),
],
args=args_odoo,
image_pull_policy='Always',
# command=['chown', '-R', '101:101', '/mnt/extra-addons'],
resources=limits,
# comment following line, if you want to run as odoo user
# security_context=client.V1SecurityContext(run_as_user=0, run_as_group=0),
volume_mounts=[
client.V1VolumeMount(name=f'{app_name}-odoo-web-pv-storage', mount_path='/var/lib/odoo/')
]
)
# pod Volume Claim
volume_claim = client.V1PersistentVolumeClaimVolumeSource(claim_name=f'{app_name}-odoo-web-pv-claim')
# pod volume
volume = client.V1Volume(name=f'{app_name}-odoo-web-pv-storage', persistent_volume_claim=volume_claim)
# Strategy
strategy = client.V1DeploymentStrategy(type='Recreate')
# Template
# for running as an odoo user changes instead of stash
spec = client.V1PodSpec(
containers=[odoo_container],
volumes=[volume],
image_pull_secrets=image_pull_secrets,
security_context=client.V1PodSecurityContext(
run_as_group=101,
run_as_user=101,
fs_group=101,
fs_group_change_policy='Always'
),
node_selector=node_selector,
)
template = client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(labels={'app': app_name, 'tier': "backend"}),
spec=spec
)
selector = client.V1LabelSelector(match_labels={'app': app_name, 'tier': 'backend'})
# Spec
specs = client.V1DeploymentSpec(
replicas=1,
strategy=strategy,
selector=selector,
template=template,
)
create_deployment(meta_data, specs, namespace, self=self)
def delete_odoo_deployment(app_name, namespace="default", self=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
dep_name = app_name + "-odoo-deployment"
core_v1_api = client.AppsV1Api()
try:
deployment = core_v1_api.delete_namespaced_deployment(name=dep_name, namespace=namespace)
if self.is_custom_image and self.docker_image:
if self.docker_image.is_pvt_dkr_repo and self.docker_image.b64_dkr_config:
delete_docker_repo_secret(app_name, namespace, self)
_logger.info(str(deployment))
except client.exceptions.ApiException as e:
_logger.error(str(e))

View File

@ -0,0 +1,315 @@
# -*- coding: utf-8 -*-
import logging
from kubernetes import client, config
from odoo.exceptions import ValidationError
from odoo.addons.smile_log.tools import SmileDBLogger
def create_ingress(app_name, self=False):
_logger = logging.getLogger(__name__)
if self:
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
# create_ingress
if not self.domain_name and self.sub_domain_name:
return ValidationError('Either Domain name or Subdomain name is not Valid')
else:
host = f'{self.sub_domain_name}.{self.domain_name}'
networking_v1_api = client.NetworkingV1Api()
rules = [
client.V1IngressRule(
host=host,
http=client.V1HTTPIngressRuleValue(
paths=[
client.V1HTTPIngressPath(
path='/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=80,
),
name=f'{app_name}-odoo-service',
)
)
),
client.V1HTTPIngressPath(
path='/longpolling/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=8072,
),
name=f'{app_name}-odoo-service',
)
)
),
]
)
)
]
odoo_urls = [host]
if self and self.custom_domain_ids:
for custom_domain in self.custom_domain_ids:
rules.append(
client.V1IngressRule(
host=custom_domain.name,
http=client.V1HTTPIngressRuleValue(
paths=[
client.V1HTTPIngressPath(
path='/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=80,
),
name=f'{app_name}-odoo-service',
)
)
),
client.V1HTTPIngressPath(
path='/longpolling/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=8072,
),
name=f'{app_name}-odoo-service',
)
)
)
]
)
)
)
odoo_urls.append(custom_domain.name)
body = client.V1Ingress(
kind='Ingress',
metadata=client.V1ObjectMeta(
name=f'{app_name}-ingress',
labels={'app': app_name},
annotations={
'kubernetes.io/ingress.class': 'nginx',
'cert-manager.io/cluster-issuer': 'letsencrypt-prod',
},
),
spec=client.V1IngressSpec(
rules=rules,
tls=[
client.V1IngressTLS(
hosts=odoo_urls,
secret_name=f'{self.app_name}tls',
)
]
)
)
try:
networking_v1_api.create_namespaced_ingress(
namespace='default',
body=body
)
except client.exceptions.ApiException as e:
_logger.error(str(e))
def delete_odoo_ingress(app_name, namespace="default", self=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
networking_v1_api = client.NetworkingV1Api()
ing_name = f'{app_name}-ingress'
try:
ing = networking_v1_api.delete_namespaced_ingress(name=ing_name, namespace=namespace)
_logger.info(str(ing))
except client.exceptions.ApiException as e:
_logger.error(str(e))
def update_odoo_ingress(app_name, namespace="default", self=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
networking_v1_api = client.NetworkingV1Api()
ing_name = f'{app_name}-ingress'
if not self.domain_name and self.sub_domain_name:
return ValidationError('Either Domain name or Subdomain name is not Valid')
else:
host = f'{self.sub_domain_name}.{self.domain_name}'
rules = [
client.V1IngressRule(
host=host,
http=client.V1HTTPIngressRuleValue(
paths=[
client.V1HTTPIngressPath(
path='/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=80,
),
name=f'{app_name}-odoo-service',
)
)
),
client.V1HTTPIngressPath(
path='/longpolling/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=8072,
),
name=f'{app_name}-odoo-service',
)
)
)
]
)
)
]
tls_hosts = [host]
if self and self.custom_domain_ids:
for custom_domain in self.custom_domain_ids:
rules.append(
client.V1IngressRule(
host=custom_domain.name,
http=client.V1HTTPIngressRuleValue(
paths=[
client.V1HTTPIngressPath(
path='/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=80,
),
name=f'{app_name}-odoo-service',
)
)
),
client.V1HTTPIngressPath(
path='/longpolling/',
path_type='ImplementationSpecific',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(
number=8072,
),
name=f'{app_name}-odoo-service',
)
)
)
]
)
)
)
tls_hosts.append(custom_domain.name)
body = client.V1Ingress(
kind='Ingress',
metadata=client.V1ObjectMeta(
name=f'{app_name}-ingress',
labels={"app": app_name},
annotations={
'kubernetes.io/ingress.class': 'nginx',
}
),
spec=client.V1IngressSpec(
rules=rules,
tls=[
client.V1IngressTLS(
hosts=tls_hosts,
secret_name=f'{self.app_name}tls'
)
]
)
)
try:
ing = networking_v1_api.patch_namespaced_ingress(name=ing_name, namespace=namespace, body=body)
_logger.info(str(ing))
except client.exceptions.ApiException as e:
_logger.error(str(e))
# ======================================================================================================================
def create_ingress_http(app_name, self=False):
_logger = logging.getLogger(__name__)
if self:
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
# create_ingress
if not self.domain_name and self.sub_domain_name:
return ValidationError('Either Domain name or Subdomain name is not Valid')
networking_v1_api = client.NetworkingV1Api()
host = f'{self.sub_domain_name}.{self.domain_name}'
chat_url = '/longpolling'
if self.docker_image.gevent_key == '--gevent-port':
chat_url = '/websocket'
rules = [
client.V1IngressRule(
host=host,
http=client.V1HTTPIngressRuleValue(
paths=[
client.V1HTTPIngressPath(
path='/',
path_type='Prefix',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(number=80),
name=f'{app_name}-odoo-service',
)
)
),
client.V1HTTPIngressPath(
path=chat_url,
path_type='Prefix',
backend=client.V1IngressBackend(
service=client.V1IngressServiceBackend(
port=client.V1ServiceBackendPort(number=8072),
name=f'{app_name}-odoo-service',
)
)
)
]
)
)
]
body = client.V1Ingress(
kind='Ingress',
metadata=client.V1ObjectMeta(
name=f'{app_name}-ingress',
labels={'app': app_name},
annotations={
'kubernetes.io/ingress.class': 'nginx',
'cert-manager.io/cluster-issuer': 'letsencrypt-prod',
'nginx.ingress.kubernetes.io/websocket-services': f'{app_name}-odoo-service',
},
),
spec=client.V1IngressSpec(
rules=rules,
)
)
try:
networking_v1_api.create_namespaced_ingress(
namespace='default',
body=body,
)
except client.exceptions.ApiException as e:
_logger.error(str(e))

View File

@ -0,0 +1,271 @@
# -*- coding: utf-8 -*-
from kubernetes import config, client
from kubernetes.stream import stream
import yaml
from odoo.exceptions import UserError
from odoo.addons.smile_log.tools import SmileDBLogger
from .pg_server import delete_databases
from .utils import generate_commit_sha
from .odoo_components import (
deploy_odoo_components,
delete_odoo_components,
delete_odoo_components_from_options,
update_odoo_components
)
def create_deployment(app_name, config_file, self=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
"""
Configs can be set in Configuration class directly or using helper
utility. If no argument provided, the config will be loaded from
default location.
"""
try:
data2 = yaml.safe_load(config_file)
config.load_kube_config_from_dict(data2)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
if app_name:
deploy_odoo_components(app_name=app_name, namespace="default", self=self)
else:
_logger.error("Cant find App Name")
raise UserError("Cant find App Name")
def delete_app_with_options(self, delete_db, delete_pv, delete_svc, delete_ing, delete_deployment):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
data = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
if self.app_name:
delete_odoo_components_from_options(
app_name=self.app_name,
namespace="default",
self=self,
delete_db=delete_db,
delete_pv=delete_pv,
delete_svc=delete_svc,
delete_ing=delete_ing,
delete_deployment=delete_deployment
)
if delete_db:
delete_databases(self)
else:
_logger.error("Cant find App Name")
raise UserError("Cant find App Name")
def update_app(self):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
data = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
if self.app_name:
update_odoo_components(app_name=self.app_name, namespace="default", self=self)
else:
_logger.error("Cant find App Name")
raise UserError("Cant find App Name")
def fetch_secrets_from_cluster(self):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
data2 = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data2)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
secs = []
if self.app_name:
core_v1_api = client.CoreV1Api()
secrs = core_v1_api.list_namespaced_secret(namespace='default')
for sec in secrs.items:
secs.append(sec.metadata.name)
return secs
def deploy_apps_from_git(self):
"""
To pull code from github inside running container
"""
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
data2 = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data2)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
if self.app_name:
core_v1_api = client.CoreV1Api()
pod = core_v1_api.list_namespaced_pod(namespace='default', label_selector='app={}'.format(self.app_name))
if self.is_extra_addon and self.extra_addons and pod and pod.items:
base_version = self.docker_image.base_version
clone_path = "/var/lib/odoo/addons/" + str(base_version)
if self.is_private_repo and self.git_token:
url = self.extra_addons
url = url.replace("http://", "")
url = url.replace("https://", "")
url = url.replace("www.", "")
git_url = "https://oauth2:{0}@{1}".format(self.git_token, url)
else:
git_url = self.extra_addons
is_clone_error = False
error = ''
exec_command = ['git', '-C', clone_path, 'pull']
resp = stream(core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=exec_command,
stderr=True, stdin=True,
stdout=True, tty=False,
_preload_content=False)
while resp.is_open():
resp.update(timeout=10)
if resp.peek_stdout():
_logger.info(str(resp.read_stdout()))
if resp.peek_stderr():
is_clone_error = True
error = resp.read_stderr()
_logger.error(str(error))
break
resp.close()
if is_clone_error:
if error and "not a git repository (or any" in error:
resp1 = stream(core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=['chmod', '-R', 'ugo+rw', clone_path],
stderr=True, stdin=False,
stdout=True, tty=False,
_preload_content=False)
resp = stream(core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=['git', 'clone', git_url, clone_path],
stderr=True, stdin=False,
stdout=True, tty=False,
_preload_content=False)
while resp.is_open():
resp.update(timeout=25)
if resp.peek_stdout():
_logger.info(str(resp.read_stdout()))
if resp.peek_stderr():
error = resp.read_stderr()
_logger.error(str(error))
else:
_logger.info(str(
"No Response"
))
resp.close()
else:
return False
def restart_odoo_service(self):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
data2 = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data2)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
if self.app_name:
core_v1_api = client.CoreV1Api()
pod = core_v1_api.list_namespaced_pod(
namespace='default',
label_selector=f'app={self.app_name}')
exec_command = ['./mnt/restart_odoo.sh']
resp = stream(
core_v1_api.connect_get_namespaced_pod_exec,
pod.items[0].metadata.name,
'default',
command=exec_command,
stderr=True,
stdin=True,
stdout=True,
tty=False,
# _preload_content=False
)
while resp.is_open():
resp.update(timeout=10)
if resp.peek_stdout():
_logger.info(str(resp.read_stdout()))
if resp.peek_stderr():
error = resp.read_stderr()
_logger.error(str(error))
break
resp.close()
def read_deployment(self, dep_type='odoo'):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
data = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
if self.app_name:
dep_name = self.app_name + "-odoo-deployment"
core_v1_api = client.AppsV1Api()
try:
deployment = core_v1_api.read_namespaced_deployment(name=dep_name, namespace='default')
if deployment:
return deployment
return
except Exception as e:
pass
else:
_logger.error("Cant find App Name")
raise UserError("Cant find App Name")
def update_deployment(self, container_arguments, dep_type='odoo', env_vars=False):
_logger = SmileDBLogger(self._cr.dbname, self._name, self.id, self._uid)
try:
data = yaml.safe_load(self.configuration.config_file)
config.load_kube_config_from_dict(data)
except config.config_exception.ConfigException as e:
_logger.error(str(e))
raise UserError("Unable to Connect K8s Cluster")
if self.app_name:
core_v1_api = client.AppsV1Api()
try:
deployment = read_deployment(self=self)
if container_arguments:
deployment.spec.template.spec.containers[0].args = eval(container_arguments)
if env_vars:
deployment.spec.template.spec.containers[0].env = env_vars
deployment.spec.template.metadata.labels['COMMIT_SHA'] = generate_commit_sha(10)
patched_deployment = core_v1_api.patch_namespaced_deployment(name=deployment.metadata.name,
namespace='default',
body=deployment)
return patched_deployment
except Exception as e:
_logger.error(str(e))
else:
_logger.error("Cant find App Name")
raise UserError("Cant find App Name")

Some files were not shown because too many files have changed in this diff Show More