From b50f9163968edc6b4cabf27e6ec066cbb3386d66 Mon Sep 17 00:00:00 2001 From: Gelson do Souto Date: Wed, 11 Mar 2026 13:06:32 +0000 Subject: [PATCH] Auto-deploy SYNCRA: syncra_welcome.zip --- syncra_welcome/__init__.py | 2 + syncra_welcome/__manifest__.py | 28 +++++++ syncra_welcome/models/__init__.py | 3 + syncra_welcome/models/syncra_devops.py | 79 +++++++++++++++++++ syncra_welcome/models/syncra_logs.py | 47 +++++++++++ syncra_welcome/models/syncra_welcome.py | 33 ++++++++ syncra_welcome/security/ir.model.access.csv | 2 + syncra_welcome/views/syncra_devops_views.xml | 45 +++++++++++ syncra_welcome/views/syncra_logs_views.xml | 24 ++++++ syncra_welcome/views/syncra_welcome_views.xml | 48 +++++++++++ syncra_welcome/wizard/__init__.py | 2 + syncra_welcome/wizard/syncra_deploy_wizard.py | 9 +++ .../wizard/syncra_deploy_wizard.xml | 15 ++++ 13 files changed, 337 insertions(+) create mode 100644 syncra_welcome/__init__.py create mode 100644 syncra_welcome/__manifest__.py create mode 100644 syncra_welcome/models/__init__.py create mode 100644 syncra_welcome/models/syncra_devops.py create mode 100644 syncra_welcome/models/syncra_logs.py create mode 100644 syncra_welcome/models/syncra_welcome.py create mode 100644 syncra_welcome/security/ir.model.access.csv create mode 100644 syncra_welcome/views/syncra_devops_views.xml create mode 100644 syncra_welcome/views/syncra_logs_views.xml create mode 100644 syncra_welcome/views/syncra_welcome_views.xml create mode 100644 syncra_welcome/wizard/__init__.py create mode 100644 syncra_welcome/wizard/syncra_deploy_wizard.py create mode 100644 syncra_welcome/wizard/syncra_deploy_wizard.xml diff --git a/syncra_welcome/__init__.py b/syncra_welcome/__init__.py new file mode 100644 index 0000000..9b42961 --- /dev/null +++ b/syncra_welcome/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/syncra_welcome/__manifest__.py b/syncra_welcome/__manifest__.py new file mode 100644 index 0000000..f1930b4 --- /dev/null +++ b/syncra_welcome/__manifest__.py @@ -0,0 +1,28 @@ +{ + 'name': 'SYNCRA Welcome', + 'version': '1.1', + 'category': 'Tools', + 'summary': 'Módulo DevOps SYNCRA para Gestão de Módulos e Logs', + 'description': """ + Módulo SYNCRA para automação de infraestrutura. + + Funcionalidades: + - Painel de Boas-Vindas com atalhos para Gitea e Cockpit. + - Automação de Deploy: Extração de ZIP e Git Push automático. + - Gestão de Logs: Visualização e Download do log oficial da VPS HilariBD. + """, + 'author': 'Gelson Lírio', + 'depends': [ + 'base', + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/syncra_welcome_views.xml', + 'views/syncra_devops_views.xml', + 'views/syncra_logs_views.xml', + ], + 'installable': True, + 'application': True, + 'auto_install': False, + 'license': 'LGPL-3', +} diff --git a/syncra_welcome/models/__init__.py b/syncra_welcome/models/__init__.py new file mode 100644 index 0000000..e5c7403 --- /dev/null +++ b/syncra_welcome/models/__init__.py @@ -0,0 +1,3 @@ +from . import syncra_welcome +from . import syncra_devops +from . import syncra_logs diff --git a/syncra_welcome/models/syncra_devops.py b/syncra_welcome/models/syncra_devops.py new file mode 100644 index 0000000..ad0e3ec --- /dev/null +++ b/syncra_welcome/models/syncra_devops.py @@ -0,0 +1,79 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +import os, zipfile, base64, io, subprocess + +class SyncraDevOps(models.Model): + _inherit = 'syncra.welcome' + + module_zip = fields.Binary(string="Upload Módulo (.zip)") + module_filename = fields.Char(string="Nome do Ficheiro") + + def action_deploy_module(self): + """Descompacta o ZIP na custom_addons2 e sincroniza com o Gitea do gelson.souto""" + if not self.module_zip: + raise UserError(_("Por favor, carregue um ficheiro .zip primeiro.")) + + addons_path = '/root/odoo-18.0+e.20251216/custom_addons2/' + # URL exato validado no terminal da HilariBD + git_url = "http://gelson.souto:Luanda244@173.208.243.178:3000/gelson.souto/syncra_addons.git" + + try: + # 1. Descompactar os ficheiros + zip_data = base64.b64decode(self.module_zip) + with zipfile.ZipFile(io.BytesIO(zip_data)) as z: + z.extractall(addons_path) + + # 2. Operações Git + os.chdir(addons_path) + + # Configurações de identidade local + subprocess.run(['git', 'config', 'user.email', 'gelson.souto@syncra.com'], check=True) + subprocess.run(['git', 'config', 'user.name', 'Gelson do Souto'], check=True) + + # Adicionar e Commit (com verificação para não falhar se não houver mudanças) + subprocess.run(['git', 'add', '.'], check=True) + + # O status verifica se há algo novo antes de tentar o commit + status = subprocess.run(['git', 'status', '--porcelain'], capture_output=True, text=True) + if status.stdout: + subprocess.run(['git', 'commit', '-m', f"Auto-deploy SYNCRA: {self.module_filename}"], check=True) + + # 3. Push Direto e Seguro + # Usamos -c credential.helper= para garantir que ele ignore senhas antigas e use o git_url + subprocess.run(['git', '-c', 'credential.helper=', 'push', git_url, 'HEAD:main'], check=True) + + # 4. Atualizar lista de módulos no Odoo + self.env['ir.module.module'].update_list() + self.module_zip = False + + return { + 'effect': { + 'fadeout': 'slow', + 'message': 'Sucesso! Módulo em custom_addons2 e Gitea atualizado.', + 'type': 'rainbow_man' + } + } + + except subprocess.CalledProcessError as e: + raise UserError(_("Erro no comando Git (Verifique o terminal): %s") % (e.stderr or str(e))) + except Exception as e: + raise UserError(_("Erro inesperado no Deploy: %s") % str(e)) + + def action_restart_odoo(self): + """Reinicia o serviço sem causar erro de SIGTERM no ecrã""" + try: + # O comando 'sleep 1' dá tempo ao Odoo para enviar o Rainbow Man antes de cair + restart_command = "sleep 1 && sudo systemctl restart odoo" + + # Executa de forma desvinculada (nohup ou fork) + subprocess.Popen(['/bin/bash', '-c', restart_command]) + + return { + 'effect': { + 'fadeout': 'slow', + 'message': 'Sinal de reinício enviado! O sistema voltará em instantes.', + 'type': 'rainbow_man', + } + } + except Exception as e: + raise UserError(_("Falha ao agendar reinício: %s") % str(e)) diff --git a/syncra_welcome/models/syncra_logs.py b/syncra_welcome/models/syncra_logs.py new file mode 100644 index 0000000..eaed24c --- /dev/null +++ b/syncra_welcome/models/syncra_logs.py @@ -0,0 +1,47 @@ +from odoo import models, fields, api, _ +import os, base64 + +class SyncraLogs(models.Model): + _inherit = 'syncra.welcome' + + system_logs = fields.Text(string="Logs do Odoo", readonly=True) + last_log_file = fields.Binary(string="Ficheiro de Log", readonly=True) + last_log_filename = fields.Char(string="Nome do Ficheiro de Log") + + def action_fetch_logs(self): + """Lê as últimas linhas do log oficial com otimização de memória""" + log_path = '/var/log/odoo/odoo.log' + try: + if os.path.exists(log_path): + # Usamos um comando de sistema (tail) que é muito mais rápido para arquivos grandes + import subprocess + result = subprocess.run(['tail', '-n', '100', log_path], capture_output=True, text=True) + logs = result.stdout + + if not logs: + self.system_logs = "O ficheiro existe mas está vazio." + else: + self.system_logs = logs + else: + self.system_logs = f"Log não encontrado em: {log_path}" + except Exception as e: + self.system_logs = f"Erro ao aceder aos logs: {str(e)}" + + def action_download_last_log(self): + """Gera o download sem comprometer a memória do servidor""" + log_path = '/var/log/odoo/odoo.log' + if os.path.exists(log_path): + with open(log_path, 'rb') as f: + # O Odoo lida bem com base64 para arquivos de texto + log_content = base64.b64encode(f.read()) + + self.write({ + 'last_log_file': log_content, + 'last_log_filename': f"syncra_vps_log_{fields.Date.today()}.txt" + }) + + return { + 'type': 'ir.actions.act_url', + 'url': f'/web/content/?model={self._name}&id={self.id}&field=last_log_file&filename={self.last_log_filename}&download=true', + 'target': 'self', + } diff --git a/syncra_welcome/models/syncra_welcome.py b/syncra_welcome/models/syncra_welcome.py new file mode 100644 index 0000000..864b2a5 --- /dev/null +++ b/syncra_welcome/models/syncra_welcome.py @@ -0,0 +1,33 @@ +from odoo import models, fields + +class SyncraWelcome(models.Model): + _name = 'syncra.welcome' + _description = 'Portal de Boas-Vindas SYNCRA' + + name = fields.Char(string="Nome do Utilizador", required=True) + welcome_message = fields.Text(string="Mensagem", default="Bem-vindo ao ecossistema SYNCRA!") + + # Adicionando os campos para o Deploy não quebrar a vista + module_zip = fields.Binary(string="Upload Módulo (.zip)") + module_filename = fields.Char(string="Nome do Ficheiro") + + def action_open_gitea(self): + return { + 'type': 'ir.actions.act_url', + 'url': 'http://173.208.243.178:3000', + 'target': 'new', + } + + def action_open_cockpit(self): + return { + 'type': 'ir.actions.act_url', + 'url': 'https://173.208.243.178:9090', + 'target': 'new', + } + + def action_open_netdata(self): + return { + 'type': 'ir.actions.act_url', + 'url': 'http://173.208.243.178:8080/', + 'target': 'new', + } diff --git a/syncra_welcome/security/ir.model.access.csv b/syncra_welcome/security/ir.model.access.csv new file mode 100644 index 0000000..53fc2f1 --- /dev/null +++ b/syncra_welcome/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_syncra_welcome_all,syncra.welcome.all,model_syncra_welcome,,1,1,1,1 diff --git a/syncra_welcome/views/syncra_devops_views.xml b/syncra_welcome/views/syncra_devops_views.xml new file mode 100644 index 0000000..b3a0ba3 --- /dev/null +++ b/syncra_welcome/views/syncra_devops_views.xml @@ -0,0 +1,45 @@ + + + + syncra.welcome.form.devops + syncra.welcome + + + +