first commit

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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