initial commit k8s_odoo_manager
This commit is contained in:
parent
3f2414a415
commit
a66f6dbbfd
13 changed files with 2256 additions and 0 deletions
4
bemade_k8s_odoo_manager/__init__.py
Normal file
4
bemade_k8s_odoo_manager/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import controllers
|
||||
68
bemade_k8s_odoo_manager/__manifest__.py
Normal file
68
bemade_k8s_odoo_manager/__manifest__.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Kubernetes Odoo Manager',
|
||||
'version': '1.0.0',
|
||||
'category': 'Administration',
|
||||
'summary': 'Manage Odoo instances running on Kubernetes',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://bemade.org',
|
||||
'license': 'LGPL-3',
|
||||
'depends': [
|
||||
'base',
|
||||
'web',
|
||||
'mail',
|
||||
],
|
||||
'data': [
|
||||
'security/k8s_odoo_manager_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/k8s_cluster_views.xml',
|
||||
'views/k8s_odoo_instance_views.xml',
|
||||
'views/k8s_odoo_manager_menus.xml',
|
||||
'data/k8s_odoo_manager_data.xml',
|
||||
'wizards/wizard_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'bemade_k8s_odoo_manager/static/src/js/instance_dashboard.js',
|
||||
'bemade_k8s_odoo_manager/static/src/scss/instance_dashboard.scss',
|
||||
'bemade_k8s_odoo_manager/static/src/xml/instance_dashboard.xml',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'auto_install': False,
|
||||
'description': """
|
||||
Kubernetes Odoo Manager
|
||||
======================
|
||||
|
||||
This module provides an interface for managing Odoo instances running on Kubernetes clusters.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
* **Kubernetes Connection**:
|
||||
- Connect to a Kubernetes cluster using kubeconfig file authentication
|
||||
- Securely store and manage kubeconfig credentials
|
||||
|
||||
* **Odoo Instance Management**:
|
||||
- Create, update, and delete Odoo instances
|
||||
- Scale instances up or down
|
||||
- Configure resources (CPU, memory, storage)
|
||||
- Manage domain names and TLS certificates
|
||||
- Set up custom configuration options
|
||||
|
||||
* **Upgrade Management**:
|
||||
- Schedule and execute module upgrades
|
||||
- Track upgrade history
|
||||
|
||||
Technical Information
|
||||
--------------------
|
||||
|
||||
This module interfaces with the Kubernetes API to manage Odoo instances using the Odoo Operator
|
||||
custom resource definition (CRD). It provides a user-friendly interface for operations that would
|
||||
otherwise require command-line tools or direct API access.
|
||||
|
||||
The module is designed to work with the Bemade Odoo Operator for Kubernetes, which handles the
|
||||
actual deployment and management of Odoo instances on the cluster.
|
||||
""",
|
||||
}
|
||||
17
bemade_k8s_odoo_manager/init/k8s_odoo_upgrade.sql
Normal file
17
bemade_k8s_odoo_manager/init/k8s_odoo_upgrade.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
CREATE OR REPLACE VIEW k8s_odoo_upgrade AS
|
||||
SELECT
|
||||
0 AS id,
|
||||
NULL::integer AS instance_id,
|
||||
NULL::integer AS cluster_id,
|
||||
NULL::varchar AS job_name,
|
||||
NULL::varchar AS display_name,
|
||||
NULL::varchar AS state,
|
||||
NULL::timestamp AS scheduled_date,
|
||||
NULL::timestamp AS start_date,
|
||||
NULL::timestamp AS end_date,
|
||||
NULL::float AS duration,
|
||||
NULL::varchar AS modules,
|
||||
NULL::varchar AS database,
|
||||
NULL::varchar AS status_message,
|
||||
NULL::text AS log
|
||||
WHERE 1=0; -- Empty view by default
|
||||
4
bemade_k8s_odoo_manager/models/__init__.py
Normal file
4
bemade_k8s_odoo_manager/models/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import k8s_cluster
|
||||
from . import k8s_odoo_instance
|
||||
311
bemade_k8s_odoo_manager/models/k8s_cluster.py
Normal file
311
bemade_k8s_odoo_manager/models/k8s_cluster.py
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
import base64
|
||||
import logging
|
||||
import tempfile
|
||||
import os
|
||||
import re
|
||||
import kubernetes.client
|
||||
import kubernetes.config
|
||||
from kubernetes.client.rest import ApiException
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class K8sCluster(models.Model):
|
||||
_name = 'k8s.cluster'
|
||||
_description = 'Kubernetes Cluster'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Name',
|
||||
required=True,
|
||||
tracking=True,
|
||||
help='Name of the Kubernetes cluster'
|
||||
)
|
||||
description = fields.Text(
|
||||
string='Description',
|
||||
tracking=True,
|
||||
help='Description of the cluster and its purpose'
|
||||
)
|
||||
active = fields.Boolean(
|
||||
string='Active',
|
||||
default=True,
|
||||
tracking=True,
|
||||
help='Whether this cluster configuration is active'
|
||||
)
|
||||
kubeconfig_attachment_id = fields.Many2one(
|
||||
'ir.attachment',
|
||||
string='Kubeconfig File',
|
||||
required=True,
|
||||
ondelete='restrict',
|
||||
domain="[('res_model', '=', 'k8s.cluster'), ('res_field', '=', 'kubeconfig_data')]",
|
||||
help='The kubeconfig file for connecting to the cluster'
|
||||
)
|
||||
kubeconfig_data = fields.Binary(
|
||||
string='Kubeconfig Data',
|
||||
attachment=True,
|
||||
required=True,
|
||||
help='The kubeconfig file content'
|
||||
)
|
||||
kubeconfig_filename = fields.Char(string='Kubeconfig Filename')
|
||||
|
||||
namespace = fields.Char(
|
||||
string='Default Namespace',
|
||||
default='default',
|
||||
required=True,
|
||||
help='The default namespace to use for operations'
|
||||
)
|
||||
|
||||
default_tls_issuer = fields.Char(
|
||||
string='Default TLS Issuer',
|
||||
default='letsencrypt-prod',
|
||||
required=True,
|
||||
help='The default cert-manager issuer to use for TLS certificates'
|
||||
)
|
||||
|
||||
connection_status = fields.Selection([
|
||||
('not_tested', 'Not Tested'),
|
||||
('connected', 'Connected'),
|
||||
('failed', 'Connection Failed')
|
||||
], string='Connection Status', default='not_tested', readonly=True)
|
||||
|
||||
connection_error = fields.Text(string='Connection Error', readonly=True)
|
||||
last_connection_test = fields.Datetime(string='Last Connection Test', readonly=True)
|
||||
|
||||
cluster_version = fields.Char(string='Kubernetes Version', readonly=True)
|
||||
node_count = fields.Integer(string='Node Count', readonly=True)
|
||||
|
||||
# Related records
|
||||
instance_count = fields.Integer(
|
||||
string='Instance Count',
|
||||
compute='_compute_instance_count',
|
||||
help='Number of Odoo instances running on this cluster'
|
||||
)
|
||||
instance_ids = fields.One2many(
|
||||
'k8s.odoo.instance',
|
||||
'cluster_id',
|
||||
string='Odoo Instances'
|
||||
)
|
||||
|
||||
@api.depends('instance_ids')
|
||||
def _compute_instance_count(self):
|
||||
for cluster in self:
|
||||
cluster.instance_count = len(cluster.instance_ids)
|
||||
|
||||
@api.constrains('namespace')
|
||||
def _check_namespace(self):
|
||||
for cluster in self:
|
||||
if not re.match(r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?$', cluster.namespace):
|
||||
raise ValidationError(_("Namespace must consist of lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character."))
|
||||
|
||||
def action_test_connection(self):
|
||||
"""Test the connection to the Kubernetes cluster"""
|
||||
self.ensure_one()
|
||||
|
||||
try:
|
||||
# Create a temporary file for the kubeconfig if needed
|
||||
temp_files = []
|
||||
|
||||
# Configure the Kubernetes client using kubeconfig
|
||||
if not self.kubeconfig_data:
|
||||
raise UserError(_('Kubeconfig file is required.'))
|
||||
|
||||
# Create a temporary file for the kubeconfig
|
||||
kubeconfig_fd, kubeconfig_path = tempfile.mkstemp()
|
||||
temp_files.append(kubeconfig_path)
|
||||
with os.fdopen(kubeconfig_fd, 'wb') as f:
|
||||
f.write(base64.b64decode(self.kubeconfig_data))
|
||||
|
||||
# Load the kubeconfig
|
||||
kubernetes.config.load_kube_config(config_file=kubeconfig_path)
|
||||
|
||||
# Create API clients
|
||||
core_api = kubernetes.client.CoreV1Api()
|
||||
version_api = kubernetes.client.VersionApi()
|
||||
|
||||
# Test the connection by getting the cluster version
|
||||
version_info = version_api.get_code()
|
||||
|
||||
# Get the node count
|
||||
nodes = core_api.list_node()
|
||||
node_count = len(nodes.items)
|
||||
|
||||
# Update the cluster information
|
||||
self.write({
|
||||
'connection_status': 'connected',
|
||||
'connection_error': False,
|
||||
'last_connection_test': fields.Datetime.now(),
|
||||
'cluster_version': f"{version_info.major}.{version_info.minor}",
|
||||
'node_count': node_count,
|
||||
})
|
||||
|
||||
# Clean up temporary files
|
||||
for temp_file in temp_files:
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Connection Test Successful'),
|
||||
'message': _('Successfully connected to Kubernetes cluster %s (version %s) with %s nodes.') % (
|
||||
self.name, self.cluster_version, self.node_count
|
||||
),
|
||||
'sticky': False,
|
||||
'type': 'success',
|
||||
}
|
||||
}
|
||||
|
||||
except ApiException as e:
|
||||
# Clean up temporary files
|
||||
for temp_file in temp_files:
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
|
||||
error_message = f"API Error: {e.reason}"
|
||||
self.write({
|
||||
'connection_status': 'failed',
|
||||
'connection_error': error_message,
|
||||
'last_connection_test': fields.Datetime.now(),
|
||||
})
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Connection Test Failed'),
|
||||
'message': error_message,
|
||||
'sticky': False,
|
||||
'type': 'danger',
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
# Clean up temporary files
|
||||
for temp_file in temp_files:
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
|
||||
error_message = f"Error: {str(e)}"
|
||||
self.write({
|
||||
'connection_status': 'failed',
|
||||
'connection_error': error_message,
|
||||
'last_connection_test': fields.Datetime.now(),
|
||||
})
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Connection Test Failed'),
|
||||
'message': error_message,
|
||||
'sticky': False,
|
||||
'type': 'danger',
|
||||
}
|
||||
}
|
||||
|
||||
def get_k8s_client(self):
|
||||
"""Get a configured Kubernetes client for this cluster"""
|
||||
self.ensure_one()
|
||||
|
||||
# Create a temporary file for the kubeconfig if needed
|
||||
temp_files = []
|
||||
|
||||
try:
|
||||
# Configure the Kubernetes client using kubeconfig
|
||||
if not self.kubeconfig_data:
|
||||
raise UserError(_('Kubeconfig file is required.'))
|
||||
|
||||
# Create a temporary file for the kubeconfig
|
||||
kubeconfig_fd, kubeconfig_path = tempfile.mkstemp()
|
||||
temp_files.append(kubeconfig_path)
|
||||
with os.fdopen(kubeconfig_fd, 'wb') as f:
|
||||
f.write(base64.b64decode(self.kubeconfig_data))
|
||||
|
||||
# Load the kubeconfig
|
||||
kubernetes.config.load_kube_config(config_file=kubeconfig_path)
|
||||
|
||||
# Create and return the API client
|
||||
return kubernetes.client.ApiClient()
|
||||
|
||||
except Exception as e:
|
||||
# Clean up temporary files
|
||||
for temp_file in temp_files:
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
|
||||
raise UserError(_("Failed to configure Kubernetes client: %s") % str(e))
|
||||
|
||||
def action_view_instances(self):
|
||||
"""View Odoo instances running on this cluster"""
|
||||
self.ensure_one()
|
||||
|
||||
return {
|
||||
'name': _('Odoo Instances'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'k8s.odoo.instance',
|
||||
'view_mode': 'tree,form',
|
||||
'domain': [('cluster_id', '=', self.id)],
|
||||
'context': {'default_cluster_id': self.id},
|
||||
}
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('name', _('New')) == _('New'):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('k8s.cluster') or _('New')
|
||||
|
||||
# Create attachment for kubeconfig
|
||||
if 'kubeconfig_data' in vals and vals.get('kubeconfig_data'):
|
||||
attachment_vals = {
|
||||
'name': vals.get('kubeconfig_filename', 'kubeconfig'),
|
||||
'type': 'binary',
|
||||
'datas': vals['kubeconfig_data'],
|
||||
'res_model': 'k8s.cluster',
|
||||
'res_field': 'kubeconfig_data',
|
||||
}
|
||||
attachment = self.env['ir.attachment'].create(attachment_vals)
|
||||
vals['kubeconfig_attachment_id'] = attachment.id
|
||||
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
# Update attachment if kubeconfig changes
|
||||
if 'kubeconfig_data' in vals and vals.get('kubeconfig_data'):
|
||||
for record in self:
|
||||
# Delete old attachment if it exists
|
||||
if record.kubeconfig_attachment_id:
|
||||
record.kubeconfig_attachment_id.unlink()
|
||||
|
||||
# Create new attachment
|
||||
attachment_vals = {
|
||||
'name': vals.get('kubeconfig_filename', record.kubeconfig_filename or 'kubeconfig'),
|
||||
'type': 'binary',
|
||||
'datas': vals['kubeconfig_data'],
|
||||
'res_model': 'k8s.cluster',
|
||||
'res_field': 'kubeconfig_data',
|
||||
}
|
||||
attachment = self.env['ir.attachment'].create(attachment_vals)
|
||||
vals['kubeconfig_attachment_id'] = attachment.id
|
||||
|
||||
return super().write(vals)
|
||||
|
||||
def unlink(self):
|
||||
# Delete attachments when records are deleted
|
||||
attachments = self.mapped('kubeconfig_attachment_id')
|
||||
result = super().unlink()
|
||||
if result and attachments:
|
||||
attachments.unlink()
|
||||
return result
|
||||
1280
bemade_k8s_odoo_manager/models/k8s_odoo_instance.py
Normal file
1280
bemade_k8s_odoo_manager/models/k8s_odoo_instance.py
Normal file
File diff suppressed because it is too large
Load diff
6
bemade_k8s_odoo_manager/security/ir.model.access.csv
Normal file
6
bemade_k8s_odoo_manager/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_k8s_cluster_admin,k8s.cluster admin,model_k8s_cluster,bemade_k8s_odoo_manager.group_k8s_odoo_manager_admin,1,1,1,1
|
||||
access_k8s_cluster_user,k8s.cluster user,model_k8s_cluster,bemade_k8s_odoo_manager.group_k8s_odoo_manager_user,1,0,0,0
|
||||
access_k8s_odoo_instance_admin,k8s.odoo.instance admin,model_k8s_odoo_instance,bemade_k8s_odoo_manager.group_k8s_odoo_manager_admin,1,1,1,1
|
||||
access_k8s_odoo_instance_user,k8s.odoo.instance user,model_k8s_odoo_instance,bemade_k8s_odoo_manager.group_k8s_odoo_manager_user,1,0,0,0
|
||||
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
<!-- Security Groups -->
|
||||
<record id="group_k8s_odoo_manager_user" model="res.groups">
|
||||
<field name="name">Kubernetes Odoo Manager / User</field>
|
||||
<field name="category_id" ref="base.module_category_administration"/>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_k8s_odoo_manager_admin" model="res.groups">
|
||||
<field name="name">Kubernetes Odoo Manager / Administrator</field>
|
||||
<field name="category_id" ref="base.module_category_administration"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_k8s_odoo_manager_user'))]"/>
|
||||
<field name="users" eval="[(4, ref('base.user_admin'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Record Rules -->
|
||||
<record id="rule_k8s_cluster_admin" model="ir.rule">
|
||||
<field name="name">Kubernetes Cluster: Administrators can see all clusters</field>
|
||||
<field name="model_id" ref="model_k8s_cluster"/>
|
||||
<field name="groups" eval="[(4, ref('group_k8s_odoo_manager_admin'))]"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="rule_k8s_cluster_user" model="ir.rule">
|
||||
<field name="name">Kubernetes Cluster: Users can only see active clusters</field>
|
||||
<field name="model_id" ref="model_k8s_cluster"/>
|
||||
<field name="groups" eval="[(4, ref('group_k8s_odoo_manager_user'))]"/>
|
||||
<field name="domain_force">[('active', '=', True)]</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="rule_k8s_odoo_instance_admin" model="ir.rule">
|
||||
<field name="name">Kubernetes Odoo Instance: Administrators can see all instances</field>
|
||||
<field name="model_id" ref="model_k8s_odoo_instance"/>
|
||||
<field name="groups" eval="[(4, ref('group_k8s_odoo_manager_admin'))]"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="rule_k8s_odoo_instance_user" model="ir.rule">
|
||||
<field name="name">Kubernetes Odoo Instance: Users can only see active instances</field>
|
||||
<field name="model_id" ref="model_k8s_odoo_instance"/>
|
||||
<field name="groups" eval="[(4, ref('group_k8s_odoo_manager_user'))]"/>
|
||||
<field name="domain_force">[('active', '=', True)]</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="rule_k8s_odoo_upgrade_admin" model="ir.rule">
|
||||
<field name="name">Kubernetes Odoo Upgrade: Administrators can see all upgrades</field>
|
||||
<field name="model_id" ref="model_k8s_odoo_upgrade"/>
|
||||
<field name="groups" eval="[(4, ref('group_k8s_odoo_manager_admin'))]"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="rule_k8s_odoo_upgrade_user" model="ir.rule">
|
||||
<field name="name">Kubernetes Odoo Upgrade: Users can only see upgrades for active instances</field>
|
||||
<field name="model_id" ref="model_k8s_odoo_upgrade"/>
|
||||
<field name="groups" eval="[(4, ref('group_k8s_odoo_manager_user'))]"/>
|
||||
<field name="domain_force">[('instance_id.active', '=', True)]</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
107
bemade_k8s_odoo_manager/views/k8s_cluster_views.xml
Normal file
107
bemade_k8s_odoo_manager/views/k8s_cluster_views.xml
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Tree View -->
|
||||
<record id="view_k8s_cluster_tree" model="ir.ui.view">
|
||||
<field name="name">k8s.cluster.tree</field>
|
||||
<field name="model">k8s.cluster</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Kubernetes Clusters">
|
||||
<field name="name"/>
|
||||
<field name="api_url"/>
|
||||
<field name="kubeconfig_attachment_id"/>
|
||||
<field name="active"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View -->
|
||||
<record id="view_k8s_cluster_form" model="ir.ui.view">
|
||||
<field name="name">k8s.cluster.form</field>
|
||||
<field name="model">k8s.cluster</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Kubernetes Cluster">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_test_connection" type="object" class="oe_stat_button" icon="fa-check-circle">
|
||||
<span>Test Connection</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Cluster Name"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="api_url"/>
|
||||
<field name="namespace"/>
|
||||
<field name="default_tls_issuer"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="kubeconfig_attachment_id" options="{'no_create': True}"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Instances">
|
||||
<field name="instance_ids">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="namespace"/>
|
||||
<field name="image"/>
|
||||
<field name="status"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Notes">
|
||||
<field name="notes"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="activity_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View -->
|
||||
<record id="view_k8s_cluster_search" model="ir.ui.view">
|
||||
<field name="name">k8s.cluster.search</field>
|
||||
<field name="model">k8s.cluster</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Kubernetes Clusters">
|
||||
<field name="name"/>
|
||||
<field name="api_url"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Active" name="group_by_active" context="{'group_by': 'active'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_k8s_cluster" model="ir.actions.act_window">
|
||||
<field name="name">Kubernetes Clusters</field>
|
||||
<field name="res_model">k8s.cluster</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="view_k8s_cluster_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new Kubernetes cluster
|
||||
</p>
|
||||
<p>
|
||||
Define Kubernetes clusters to connect to for managing Odoo instances.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Items -->
|
||||
<menuitem id="menu_k8s_odoo_manager_root" name="K8s Odoo Manager" web_icon="bemade_k8s_odoo_manager,static/description/icon.png" sequence="90"/>
|
||||
|
||||
<menuitem id="menu_k8s_cluster" name="Kubernetes Clusters" parent="menu_k8s_odoo_manager_root" action="action_k8s_cluster" sequence="10"/>
|
||||
</odoo>
|
||||
216
bemade_k8s_odoo_manager/views/k8s_odoo_instance_views.xml
Normal file
216
bemade_k8s_odoo_manager/views/k8s_odoo_instance_views.xml
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Tree View -->
|
||||
<record id="view_k8s_odoo_instance_tree" model="ir.ui.view">
|
||||
<field name="name">k8s.odoo.instance.tree</field>
|
||||
<field name="model">k8s.odoo.instance</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Odoo Instances" decoration-success="k8s_status=='running'" decoration-info="k8s_status=='pending'" decoration-warning="k8s_status=='upgrading'" decoration-danger="k8s_status=='error'">
|
||||
<field name="name"/>
|
||||
<field name="cluster_id"/>
|
||||
<field name="namespace"/>
|
||||
<field name="image"/>
|
||||
<field name="k8s_status" string="Status"/>
|
||||
<field name="k8s_exists"/>
|
||||
<field name="k8s_pods_count"/>
|
||||
<field name="k8s_ready_pods_count"/>
|
||||
<field name="odoo_url" widget="url"/>
|
||||
<field name="last_status_check"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View -->
|
||||
<record id="view_k8s_odoo_instance_form" model="ir.ui.view">
|
||||
<field name="name">k8s.odoo.instance.form</field>
|
||||
<field name="model">k8s.odoo.instance</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Odoo Instance">
|
||||
<header>
|
||||
<button name="action_deploy" string="Deploy" type="object" class="oe_highlight" invisible="status not in ('draft', 'error')"/>
|
||||
<button name="action_apply_changes" string="Apply Changes" type="object" class="oe_highlight" invisible="status not in ('running', 'error')"/>
|
||||
<button name="action_refresh_status" string="Refresh Status" type="object" invisible="status == 'draft'"/>
|
||||
<button name="action_view_upgrades" string="View Upgrades" type="object" invisible="status not in ('running', 'error')"/>
|
||||
<field name="status" widget="statusbar" statusbar_visible="draft,pending,running,error,upgrading"/>
|
||||
<field name="k8s_read_failed" invisible="1"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_upgrades" type="object" class="oe_stat_button" icon="fa-refresh" invisible="status not in ('running', 'error')">
|
||||
<field name="upgrade_ids" widget="statinfo" string="Upgrades"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="alert alert-danger" role="alert" invisible="not k8s_read_failed">
|
||||
<p>
|
||||
<strong>Warning:</strong> Unable to read current configuration from Kubernetes. The current configuration values are not available. Please check your connection and click "Refresh Status" to try again.</p>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Instance Name"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="cluster_id" options="{'no_create': True}"/>
|
||||
<field name="namespace"/>
|
||||
<field name="description"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="odoo_url" widget="url"/>
|
||||
<field name="status_message" readonly="1"/>
|
||||
<field name="last_status_check" readonly="1"/>
|
||||
<field name="master_password" password="True" placeholder="Set password for deployment and updates"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Docker Image">
|
||||
<group>
|
||||
<group string="Desired Configuration">
|
||||
<field name="image" readonly="status not in ('draft', 'running', 'error')"/>
|
||||
<field name="image_pull_secret" readonly="status not in ('draft', 'running', 'error')"/>
|
||||
</group>
|
||||
<group string="Current Kubernetes Configuration">
|
||||
<field name="k8s_image" readonly="1"/>
|
||||
<field name="k8s_image_pull_secret" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<div class="alert alert-info" role="alert" invisible="status not in ('running', 'error')">
|
||||
<p>
|
||||
<strong>Note:</strong> Edit the desired configuration fields and click "Apply Changes" to update the Kubernetes instance.</p>
|
||||
</div>
|
||||
</page>
|
||||
<page string="Resources">
|
||||
<group>
|
||||
<group string="Desired CPU">
|
||||
<field name="cpu_request" readonly="status not in ('draft', 'running', 'error')"/>
|
||||
<field name="cpu_limit" readonly="status not in ('draft', 'running', 'error')"/>
|
||||
</group>
|
||||
<group string="Current CPU">
|
||||
<field name="k8s_cpu_request" readonly="1"/>
|
||||
<field name="k8s_cpu_limit" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Desired Memory">
|
||||
<field name="memory_request" readonly="status not in ('draft', 'running', 'error')"/>
|
||||
<field name="memory_limit" readonly="status not in ('draft', 'running', 'error')"/>
|
||||
</group>
|
||||
<group string="Current Memory">
|
||||
<field name="k8s_memory_request" readonly="1"/>
|
||||
<field name="k8s_memory_limit" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group string="Desired Filestore">
|
||||
<field name="filestore_size" readonly="status not in ('draft', 'running', 'error')"/>
|
||||
<field name="filestore_storage_class" readonly="status not in ('draft', 'running', 'error')"/>
|
||||
</group>
|
||||
<group string="Current Filestore">
|
||||
<field name="k8s_filestore_size" readonly="1"/>
|
||||
<field name="k8s_filestore_storage_class" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<div class="alert alert-info" role="alert" invisible="status not in ('running', 'error')">
|
||||
<p>
|
||||
<strong>Note:</strong> Edit the desired configuration fields and click "Apply Changes" to update the Kubernetes instance.</p>
|
||||
</div>
|
||||
</page>
|
||||
<page string="Ingress">
|
||||
<group>
|
||||
<group string="Desired Ingress Configuration">
|
||||
<field name="host_names" readonly="status not in ('draft', 'running', 'error')"/>
|
||||
<field name="tls_issuer" readonly="status not in ('draft', 'running', 'error')" placeholder="Uses cluster's default if empty"/>
|
||||
</group>
|
||||
<group string="Current Ingress Configuration">
|
||||
<field name="k8s_host_names" readonly="1"/>
|
||||
<field name="k8s_tls_issuer" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<div class="alert alert-info" role="alert" invisible="status not in ('running', 'error')">
|
||||
<p>
|
||||
<strong>Note:</strong> Edit the desired configuration fields and click "Apply Changes" to update the Kubernetes instance.</p>
|
||||
</div>
|
||||
</page>
|
||||
<page string="Configuration">
|
||||
<group>
|
||||
<group string="Desired Odoo Configuration" colspan="1">
|
||||
<field name="config_options" widget="ace" options="{'mode': 'yaml'}" readonly="status not in ('draft', 'running', 'error')"/>
|
||||
</group>
|
||||
<group string="Current Odoo Configuration" colspan="1">
|
||||
<field name="k8s_config_options" widget="ace" options="{'mode': 'yaml'}" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<div class="alert alert-info" role="alert" invisible="status not in ('running', 'error')">
|
||||
<p>
|
||||
<strong>Note:</strong> Edit the desired configuration fields and click "Apply Changes" to update the Kubernetes instance.</p>
|
||||
</div>
|
||||
</page>
|
||||
<page string="Kubernetes Status">
|
||||
<group>
|
||||
<group string="Current Status">
|
||||
<field name="k8s_exists" readonly="1"/>
|
||||
<field name="k8s_status" readonly="1"/>
|
||||
<field name="k8s_status_message" readonly="1"/>
|
||||
</group>
|
||||
<group string="Pods">
|
||||
<field name="k8s_pods_count" readonly="1"/>
|
||||
<field name="k8s_ready_pods_count" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="activity_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View -->
|
||||
<record id="view_k8s_odoo_instance_search" model="ir.ui.view">
|
||||
<field name="name">k8s.odoo.instance.search</field>
|
||||
<field name="model">k8s.odoo.instance</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Odoo Instances">
|
||||
<field name="name"/>
|
||||
<field name="cluster_id"/>
|
||||
<field name="namespace"/>
|
||||
<field name="image"/>
|
||||
<separator/>
|
||||
<filter string="Draft" name="draft" domain="[('status', '=', 'draft')]"/>
|
||||
<filter string="Pending" name="pending" domain="[('status', '=', 'pending')]"/>
|
||||
<filter string="Running" name="running" domain="[('status', '=', 'running')]"/>
|
||||
<filter string="Error" name="error" domain="[('status', '=', 'error')]"/>
|
||||
<filter string="Upgrading" name="upgrading" domain="[('status', '=', 'upgrading')]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Cluster" name="group_by_cluster" context="{'group_by': 'cluster_id'}"/>
|
||||
<filter string="Namespace" name="group_by_namespace" context="{'group_by': 'namespace'}"/>
|
||||
<filter string="Status" name="group_by_status" context="{'group_by': 'status'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_k8s_odoo_instance" model="ir.actions.act_window">
|
||||
<field name="name">Odoo Instances</field>
|
||||
<field name="res_model">k8s.odoo.instance</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="view_k8s_odoo_instance_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new Odoo instance
|
||||
</p>
|
||||
<p>
|
||||
Create and manage Odoo instances running on Kubernetes.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Item -->
|
||||
<menuitem id="menu_k8s_odoo_instance" name="Odoo Instances" parent="menu_k8s_odoo_manager_root" action="action_k8s_odoo_instance" sequence="20"/>
|
||||
</odoo>
|
||||
7
bemade_k8s_odoo_manager/views/views.xml
Normal file
7
bemade_k8s_odoo_manager/views/views.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Include all view files -->
|
||||
<data>
|
||||
<!-- This is a placeholder file that includes all other view files -->
|
||||
</data>
|
||||
</odoo>
|
||||
3
bemade_k8s_odoo_manager/wizards/__init__.py
Normal file
3
bemade_k8s_odoo_manager/wizards/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import k8s_odoo_instance_update_wizard
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from kubernetes.client.rest import ApiException
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class K8sOdooUpgradeSimpleWizard(models.TransientModel):
|
||||
_name = 'k8s.odoo.upgrade.simple.wizard'
|
||||
_description = 'Simple Odoo Upgrade Wizard'
|
||||
|
||||
instance_id = fields.Many2one(
|
||||
'k8s.odoo.instance',
|
||||
string='Odoo Instance',
|
||||
required=True,
|
||||
readonly=True
|
||||
)
|
||||
|
||||
database = fields.Char(
|
||||
string='Database',
|
||||
required=True,
|
||||
help='Database name to upgrade'
|
||||
)
|
||||
|
||||
modules = fields.Char(
|
||||
string='Modules',
|
||||
required=True,
|
||||
help='Comma-separated list of modules to upgrade'
|
||||
)
|
||||
|
||||
scheduled_time = fields.Datetime(
|
||||
string='Scheduled Time',
|
||||
help='When to run the upgrade (leave empty to run immediately)'
|
||||
)
|
||||
|
||||
admin_password = fields.Char(
|
||||
string='Admin Password',
|
||||
required=True,
|
||||
help='Admin password for the Odoo instance'
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
active_id = self.env.context.get('active_id')
|
||||
if active_id:
|
||||
instance = self.env['k8s.odoo.instance'].browse(active_id)
|
||||
res['instance_id'] = instance.id
|
||||
# Try to get the database name from the instance
|
||||
if instance.k8s_config_options:
|
||||
import yaml
|
||||
try:
|
||||
config = yaml.safe_load(instance.k8s_config_options)
|
||||
if config and isinstance(config, dict) and 'db_name' in config:
|
||||
res['database'] = config['db_name']
|
||||
except Exception as e:
|
||||
_logger.warning(f"Could not parse config options: {e}")
|
||||
return res
|
||||
|
||||
def action_trigger_upgrade(self):
|
||||
"""Trigger the upgrade in Kubernetes"""
|
||||
self.ensure_one()
|
||||
instance = self.instance_id
|
||||
|
||||
try:
|
||||
# Get the Kubernetes client
|
||||
api_client = instance.cluster_id.get_k8s_client()
|
||||
custom_api = instance.cluster_id._get_custom_objects_api()
|
||||
|
||||
# Get the OdooInstance custom resource
|
||||
try:
|
||||
current_instance = custom_api.get_namespaced_custom_object(
|
||||
group="bemade.org",
|
||||
version="v1",
|
||||
namespace=instance.namespace,
|
||||
plural="odooinstances",
|
||||
name=instance.name,
|
||||
)
|
||||
except ApiException as e:
|
||||
if e.status == 404:
|
||||
raise UserError(
|
||||
_(
|
||||
"The Odoo instance does not exist in Kubernetes."
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise
|
||||
|
||||
# Set the admin password for the upgrade
|
||||
current_instance["spec"]["adminPassword"] = self.admin_password
|
||||
|
||||
# Prepare the upgrade spec
|
||||
upgrade_spec = {
|
||||
"database": self.database,
|
||||
"modules": [m.strip() for m in self.modules.split(',') if m.strip()]
|
||||
}
|
||||
|
||||
# Add scheduled time if provided
|
||||
if self.scheduled_time:
|
||||
upgrade_spec["time"] = self.scheduled_time.isoformat()
|
||||
|
||||
# Add the upgrade spec to trigger the operator's upgrade process
|
||||
current_instance["spec"]["upgrade"] = upgrade_spec
|
||||
|
||||
# Update the OdooInstance in Kubernetes
|
||||
custom_api.patch_namespaced_custom_object(
|
||||
group="bemade.org",
|
||||
version="v1",
|
||||
namespace=instance.namespace,
|
||||
plural="odooinstances",
|
||||
name=instance.name,
|
||||
body=current_instance,
|
||||
)
|
||||
|
||||
# Schedule a status check
|
||||
self.env.ref(
|
||||
"bemade_k8s_odoo_manager.ir_cron_check_instance_status"
|
||||
).method_direct_trigger()
|
||||
|
||||
return {
|
||||
"type": "ir.actions.client",
|
||||
"tag": "display_notification",
|
||||
"params": {
|
||||
"title": _("Upgrade Triggered"),
|
||||
"message": _("The upgrade process has been initiated."),
|
||||
"sticky": False,
|
||||
"type": "success",
|
||||
"next": {
|
||||
"type": "ir.actions.act_window_close",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_message = f"Error: {str(e)}"
|
||||
_logger.error(error_message)
|
||||
return {
|
||||
"type": "ir.actions.client",
|
||||
"tag": "display_notification",
|
||||
"params": {
|
||||
"title": _("Error"),
|
||||
"message": error_message,
|
||||
"sticky": True,
|
||||
"type": "danger",
|
||||
},
|
||||
}
|
||||
Loading…
Reference in a new issue