first commit
|
|
@ -0,0 +1,5 @@
|
|||
__pycache__
|
||||
*/__pycache__
|
||||
*/*/__pycache__
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 "لا يبدو أن هذا عنوان بريد إلكتروني."
|
||||
|
|
@ -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 ""
|
||||
|
|
@ -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."
|
||||
|
|
@ -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."
|
||||
|
|
@ -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."
|
||||
|
|
@ -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."
|
||||
|
|
@ -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."
|
||||
|
|
@ -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."
|
||||
|
|
@ -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"
|
||||
|
|
@ -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."
|
||||
|
|
@ -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."
|
||||
|
|
@ -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."
|
||||
|
|
@ -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*.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
* Install `email_validator <https://pypi.org/project/email-validator/>`_
|
||||
with ``pip install email_validator`` or equivalent.
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
To use this module, you need to:
|
||||
|
||||
* Log out.
|
||||
* `Sign up </web/signup>`_ with a valid email.
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -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 > General Settings -> 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 <<a class="reference external" href="mailto:rafaelbn@antiun.com">rafaelbn@antiun.com</a>></li>
|
||||
<li>Jairo Llopis <<a class="reference external" href="mailto:yajo.sk8@gmail.com">yajo.sk8@gmail.com</a>></li>
|
||||
<li>Simone Orsi <<a class="reference external" href="mailto:simone.orsi@camptocamp.com">simone.orsi@camptocamp.com</a>></li>
|
||||
<li>Alexandre Díaz <<a class="reference external" href="mailto:alexandre.diaz@tecnativa.com">alexandre.diaz@tecnativa.com</a>></li>
|
||||
<li>Eugene Molotov <<a class="reference external" href="mailto:molotov@it-projects.info">molotov@it-projects.info</a>></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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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"]'))
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import wizards
|
||||
|
|
@ -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,
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import saas_app
|
||||
from . import models
|
||||
|
|
@ -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))
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
After Width: | Height: | Size: 4.9 KiB |
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import backup_restore
|
||||
|
|
@ -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")
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import utils
|
||||
from . import wizards
|
||||
|
|
@ -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,
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import logs_viewer
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}" <${(object.admin_user.company_id.email ) | safe}></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>
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
|
|
@ -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...!')
|
||||
|
|
@ -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/
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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]
|
||||
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
After Width: | Height: | Size: 116 KiB |
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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); }
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
};
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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()
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import k8s_deployment
|
||||
|
|
@ -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"))
|
||||
|
|
@ -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))
|
||||
|
|
@ -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))
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
|
|
@ -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))
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import k8s_deployment
|
||||
|
|
@ -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"))
|
||||
|
|
@ -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))
|
||||
|
|
@ -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))
|
||||
|
|
@ -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")
|
||||