Auto-deploy SYNCRA: syncra_welcome.zip

This commit is contained in:
Gelson do Souto 2026-03-11 13:06:32 +00:00
parent fa35f6be60
commit b50f916396
13 changed files with 337 additions and 0 deletions

View File

@ -0,0 +1,2 @@
from . import models
from . import wizard

View File

@ -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',
}

View File

@ -0,0 +1,3 @@
from . import syncra_welcome
from . import syncra_devops
from . import syncra_logs

View File

@ -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))

View File

@ -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',
}

View File

@ -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',
}

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_syncra_welcome_all syncra.welcome.all model_syncra_welcome 1 1 1 1

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_syncra_welcome_form_devops" model="ir.ui.view">
<field name="name">syncra.welcome.form.devops</field>
<field name="model">syncra.welcome</field>
<field name="inherit_id" ref="syncra_welcome.view_syncra_welcome_form"/>
<field name="arch" type="xml">
<xpath expr="//header[@id='syncra_header']" position="inside">
<button name="action_deploy_module"
string="Executar Deploy (.zip)"
type="object"
class="btn-success"
icon="fa-rocket"/>
<button name="action_restart_odoo"
string="Reiniciar Servidor"
type="object"
class="btn-danger"
icon="fa-refresh"
confirm="Atenção: Isto irá reiniciar o serviço Odoo na VPS HilariBD. O sistema ficará temporariamente inacessível. Deseja continuar?"/>
</xpath>
<xpath expr="//notebook[@id='syncra_notebook']" position="inside">
<page string="Deploy de Módulos" name="deploy">
<group>
<separator string="Enviar Novo Módulo para Gitea"/>
<field name="module_filename" invisible="1"/>
<field name="module_zip" filename="module_filename" widget="binary"/>
<div class="alert alert-info" role="alert" style="margin-top: 20px;">
<i class="fa fa-info-circle"/>
A descompactação automática será feita no diretório:
<code style="color: #e83e8c;">/root/odoo-18.0+e.20251216/custom_addons2/</code>
</div>
<p class="text-muted">
<strong>Nota:</strong> Após o deploy bem-sucedido, utilize o botão
<span class="text-danger">"Reiniciar Servidor"</span> no topo para aplicar as mudanças de código Python.
</p>
</group>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_syncra_welcome_form_logs" model="ir.ui.view">
<field name="name">syncra.welcome.form.logs</field>
<field name="model">syncra.welcome</field>
<field name="inherit_id" ref="syncra_welcome.view_syncra_welcome_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook[@id='syncra_notebook']" position="inside">
<page string="Consola de Logs" name="logs">
<header>
<button name="action_fetch_logs" string="Ler Últimos Logs"
type="object" icon="fa-refresh" class="btn-secondary"/>
<button name="action_download_last_log" string="Baixar Log Completo (.txt)"
type="object" icon="fa-download" class="btn-primary"/>
</header>
<group>
<field name="last_log_filename" invisible="1"/>
<field name="system_logs" widget="ace" options="{'mode': 'python'}" nolabel="1"/>
</group>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_syncra_welcome" model="ir.actions.act_window">
<field name="name">Bem-vindo ao SYNCRA</field>
<field name="res_model">syncra.welcome</field>
<field name="view_mode">list,form</field>
</record>
<record id="view_syncra_welcome_list" model="ir.ui.view">
<field name="name">syncra.welcome.list</field>
<field name="model">syncra.welcome</field>
<field name="arch" type="xml">
<list string="Boas-Vindas">
<field name="name"/>
<field name="welcome_message"/>
</list>
</field>
</record>
<record id="view_syncra_welcome_form" model="ir.ui.view">
<field name="name">syncra.welcome.form</field>
<field name="model">syncra.welcome</field>
<field name="arch" type="xml">
<form string="Boas-Vindas">
<header id="syncra_header">
<button name="action_open_gitea" string="Aceder Gitea SYNCRA" type="object" class="oe_highlight" icon="fa-code"/>
<button name="action_open_cockpit" string="Monitorizar Servidor" type="object" icon="fa-dashboard"/>
<button name="action_open_netdata" string="NetData Servidor" type="object" icon="fa-dashboard"/>
</header>
<sheet>
<div class="oe_title">
<h1><field name="name" placeholder="Ex: Mensagem para o modulo"/></h1>
</div>
<notebook id="syncra_notebook">
<page string="Geral" name="general">
<group>
<field name="welcome_message" widget="text"/>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<menuitem id="menu_syncra_root" name="SYNCRA" sequence="10"/>
<menuitem id="menu_syncra_welcome" name="Boas-Vindas" parent="menu_syncra_root" action="action_syncra_welcome" sequence="10"/>
</odoo>

View File

@ -0,0 +1,2 @@
from . import syncra_deploy_wizardy

View File

@ -0,0 +1,9 @@
class SyncraDeployWizard(models.TransientModel):
_name = 'syncra.deploy.wizard'
_description = 'Assistente de Deploy SYNCRA'
welcome_id = fields.Many2one('syncra.welcome', string="Origem")
commit_message = fields.Char(string="Mensagem de Commit", required=True, default="Update modulo via SYNCRA")
def action_confirm_deploy(self):
return self.welcome_id.with_context(commit_msg=self.commit_message).action_deploy_module()

View File

@ -0,0 +1,15 @@
<record id="view_syncra_deploy_wizard_form" model="ir.ui.view">
<field name="name">syncra.deploy.wizard.form</field>
<field name="model">syncra.deploy.wizard</field>
<field name="arch" type="xml">
<form string="Confirmar Deploy">
<group>
<field name="commit_message" placeholder="Ex: Adicionado Commit"/>
</group>
<footer>
<button name="action_confirm_deploy" string="Executar Deploy" type="object" class="btn-primary"/>
<button string="Cancelar" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>