Auto-deploy SYNCRA: syncra_welcome.zip
This commit is contained in:
parent
fa35f6be60
commit
b50f916396
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import models
|
||||||
|
from . import wizard
|
||||||
|
|
@ -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',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from . import syncra_welcome
|
||||||
|
from . import syncra_devops
|
||||||
|
from . import syncra_logs
|
||||||
|
|
@ -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))
|
||||||
|
|
@ -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',
|
||||||
|
}
|
||||||
|
|
@ -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',
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import syncra_deploy_wizardy
|
||||||
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue