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