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

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

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

View File

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

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

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

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

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

View File

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

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

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

View File

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

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

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

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

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

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

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

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

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

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

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