# -*- coding: utf-8 -*- from openerp import models, fields, api from openerp.tools import email_send from openerp.exceptions import Warning, ValidationError from jinja2 import Environment as JinjaEnv, PackageLoader as JinjaLoader from ..utils.docker_api import ( get_all_external_ports, run_command, copy_in, run_container, get_internal_ip, get_status, stop_container, start_container, restart_container ) from ..utils.command import execute, list_files_and_folders from random import randint from git.util import join_path from git.repo.base import Repo from git.exc import GitCommandError import unicodedata import stringcase import time import os class OdooInstance(models.Model): _name = 'odoo.instance' CONTAINER_STATUS = [ ('draft', 'Por activar'), ('activated', 'Activado'), ('disapproved', 'No aprobado'), ('suspended', 'Suspendido'), ('destroyed', 'Eliminado') ] DEFAULT_DIRS = [ 'config', 'custom-addons', 'files' ] # 'config: /etc/odoo, custom-addons: /mnt/extra-addons, files: /var/lib/odoo' def _snakeike_name(self, name): try: name = unicodedata.normalize('NFKD', name) name = name.encode('ASCII', 'ignore') except TypeError: pass name = stringcase.trimcase(name) name = stringcase.lowercase(name) name = stringcase.snakecase(name) return name def _check_port_availability(self, port): return port not in get_all_external_ports() def _check_name_availability(self, name): if len(name) == 0: return False root_path = self.env['odoo.management.config'].get_default_settings([]).get('odoo_root_path') if len(root_path) == 0: raise Warning('La ruta por defecto para los sistemas no está definido') full_path = os.path.join(root_path, name) return not os.path.exists(full_path) def _randomize_port(self): ports = self.env['odoo.management.config'].get_default_settings([]).get('odoo_ports_range') ports = filter(lambda x: x != '', ports.split(',')) if len(ports) == 0: ports = [10000, 20000] # raise Warning('El rango de puertos para los sistemas no está definido') port = 0 while not self._check_port_availability(port): port = randint(ports[0], ports[1]) time.sleep(0.05) return port def _make_default_dirs(self, name): root_path = self.env['ir.values'].get_default(self._name, 'odoo_root_path') for d in self.DEFAULT_DIRS: full_path = os.path.join(root_path, name, d.strip()) os.makedirs(full_path) def _delete_dirs(self, name): root_path = self.env['ir.values'].get_default(self._name, 'odoo_root_path') full_path = os.path.join(root_path, name) execute(['rm', '-Rf', full_path]) def _make_config_file(self, name): root_path = self.env['ir.values'].get_default(self._name, 'odoo_root_path') config_path = os.path.join(root_path, name, 'config') if not os.path.exists(config_path): raise Warning('No se creó el directorio para el archivo de configuración') env = JinjaEnv(loader=JinjaLoader('assets', 'templates')) tmpl = env.get_template('openerp-server.j2') tmpl_rendered = tmpl.stream({ 'admin_password': 'admin', 'db_host': '', 'db_port': '', 'db_name': '', 'db_user': '', 'db_password': '' }) tmpl_rendered.dump(os.path.join(config_path, 'openerp-config.conf')) def _create_database(self, name): odoo_db = self.env['ir.values'].get_default(self._name, 'odoo_db_link') cmd = 'createdb -U %s %s' % ('odoo', name) output = run_command(odoo_db, cmd) return output def _drop_database(self, name): odoo_db = self.env['ir.values'].get_default(self._name, 'odoo_db_link') terminate_cmd = "psql -U %s select pg_terminate_backend(pid) from pg_stats where datname = '%s'" % ('odoo', name) run_command(odoo_db, terminate_cmd) drop_cmd = 'drop database %s' % name run_command(odoo_db, drop_cmd) def _copy_database_seed(self): odoo_db = self.env['ir.values'].get_default(self._name, 'odoo_db_link') backup_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'files', 'odoo.tar') output = copy_in(odoo_db, '/tmp', backup_path) return output def _restore_database(self, name): odoo_db = self.env['ir.values'].get_default(self._name, 'odoo_db_link') cmd = 'psql -U %s -d %s -f %s' % (odoo_db, name, '/tmp/odoo.sql') output = run_command(odoo_db, cmd) return output def _remove_database_seed(self): odoo_db = self.env['ir.values'].get_default(self._name, 'odoo_db_link') cmd = 'rm -f %s' % '/tmp/odoo.sql' output = run_command(odoo_db, cmd) return output def _create_container(self, name, ports=[]): ir_values = self.env['ir.values'] root_path = ir_values.get_default(self._name, 'odoo_root_path') odoo_docker_image = ir_values.get_default(self._name, 'odoo_docker_image') odoo_network = ir_values.get_default(self._name, 'odoo_network') ports = dict(map(lambda p: ('%d/tcp' % 8069, p), ports)) volumes = dict() run_container( odoo_docker_image, name, ports, volumes, odoo_network, '150m', '150m' ) time.sleep(3) return True def _apply_permissions(self, name): root_path = self.env['ir.values'].get_default(self._name, 'odoo_root_path') full_path = os.path.join(root_path, name) execute(['chmod', '-Rf', '777'], full_path) return True def _get_internal_ip(self, name): return get_internal_ip(name) def _clone_repositories(self, names=[], path=None, branch='master'): if len(names) == 0 or not path: return False git_path = self.env['ir.values'].get_default(self._name, 'modules_git_path') paths = list_files_and_folders(git_path) if len(git_path) == 0: raise Warning('La ruta por defecto del repositorio no está definido') for path in paths: for name in names: repo_path = '%s.git' % join_path(git_path, path, name) if not os.path.exists(repo_path): continue try: repo = Repo(repo_path) repo.clone(join_path(path, name), branch=branch) repo.close() except GitCommandError: pass return True def _clone_repository(self, name, path): result = self._clone_repositories([name], path) return result def _copy_modules(self, name, modules=[]): root_path = self.env['ir.values'].get_default(self._name, 'odoo_root_path') if not os.path.exists(root_path): raise Warning('El directorio del sistema no existe') container_is_running = get_status(name) if container_is_running: stopped = stop_container(name) if not stopped: raise Warning('No se pudo parar el contenedor') for module in modules: module_path = os.path.join(root_path, 'custom-addons', module) if os.path.exists(module_path): execute(['rm', '-Rf', module_path]) addons_path = os.path.join(root_path, 'custom-addons') self._clone_repository(module, addons_path) git_data_path = os.path.join(module, '.git') if os.path.exists(git_data_path): execute(['rm', '-Rf', git_data_path]) try: execute(['chmod', '-Rf', '777', module_path]) except Exception: pass if container_is_running: started = start_container(name) if not started: raise Warning('No se pudo arrancar el contenedor') def _copy_module(self, name, module): self._copy_modules(name, [module]) # def _handle_container_event(self, e): # event_type = e.get('Type') # event_action = e.get('Action') # container_id = e.get('Actor').get('ID') # # print(event_type) # print(event_action) # print(container_id) # # def _register_hook(self, cr): # run_watchdog(self._handle_container_event) # super(OdooInstance, self)._register_hook(cr) name = fields.Char(string='Nombre', size=50) normalized_name = fields.Char(string='Nombre normalizado', compute='_normalize_name', size=50) logo = fields.Binary(string='Logo') internal_ip = fields.Char(string='IP interno', size=15) internal_port = fields.Integer(string='Puerto interno') external_ip = fields.Char(string='IP externo', size=15) external_port = fields.Integer(string='Puerto externo') expose_ip = fields.Boolean(string='Exponer IP', default=True) state = fields.Selection(string='Estado', selection=CONTAINER_STATUS, default='draft') demo = fields.Boolean(string='Es un demo?', default=False) domain = fields.Char(string='Dominio', size=100) running = fields.Boolean(string='Está online?', default=False) payment_plan_id = fields.Many2one(string='Plan de pago', comodel_name='payment.plan', required=True) def _send_email(self, ): email_send() @api.model def create(self, values): snaked_name = self._snakeike_name(values.get('name')) name_is_available = self._check_name_availability(snaked_name) if not name_is_available: raise ValidationError('El nombre ya está siendo usado por otro sistema') return super(OdooInstance, self).create(values) @api.one @api.onchange('name') def _onchange_name(self): if self.name: self.name = self.name.title() @api.one @api.depends('name') def _normalize_name(self): self.normalized_name = self._snakeike_name(self.name) @api.one def action_activate(self): if self.state not in ('draft', 'suspended'): raise Warning('No se puede activar un sistema ya activo') # 1. Check name name_is_available = self._check_name_availability(self.normalized_name) if not name_is_available: raise Warning('El nombre ya está siendo usado por otro sistema') # 2. Get a port port_to_use = self._randomize_port() # 3. Create dirs make_ok = self.make_default_dirs(self.normalized_name) if not make_ok: raise Warning('No se pudo crear la estructura de directorios') # 4. Create configuration file config_ok = self.make_config_file(self.normalized_name) if not config_ok: raise Warning('No se pudo crear el archivo de configuración') # 5. Create database db_ok = self.create_database(self.normalized_name) if not db_ok: raise Warning('No se pudo crear la base de datos') # 6. Copy database seed seed_copied = self.copy_database_seed() if not seed_copied: raise Warning('No se pudo copiar la estructura inicial de la base de datos') # 7. Restore database schema restored = self.restore_database(self.normalized_name) if not restored: raise Warning('No se pudo reestablecer la copia de base de datos') # 8. Remove database seed seed_removed = self.remove_database_seed() if not seed_removed: raise Warning('No se pudo remover la copia de base de datos') # 9. Create odoo container odoo_created = self.create_container(self.normalized_name, [port_to_use]) if not odoo_created: raise Warning('No se pudo crear el contenedor') # 10. Apply permissions permissions_applied = self.apply_permissions(self.normalized_name) if not permissions_applied: raise Warning('No se pudo aplicar los permisos correspondientes') # 11. Get internal ip internal_ip = self.get_internal_ip(self.normalized_name) # 12. Send email # TODO print(internal_ip) self.state = 'activated' @api.one def action_disapprove(self): if self.state != 'draft': raise Warning('No se puede desaprobar un sistema ya activo') self.state = 'disapproved' @api.one def action_suspend(self): if self.state != 'activated': raise Warning('No se puede suspender un sistema no activo') # 1. Get container status and stop it container_is_running = get_status(self.normalized_name) if container_is_running: stopped = stop_container(self.normalized_name) if not stopped: raise Warning('No se pudo parar el contenedor %s' % self.normalized_name) self.state = 'suspended' self.running = False @api.one def copy(self): raise Warning('Atención', 'Está prohibido duplicar una instancia. Por favor, cree uno nuevo') @api.one def action_destroy(self): if self.state == 'destroyed': raise Warning('No se puede eliminar un sistema ya eliminado') # 1. Get container status and stop it if necessary container_is_running = get_status(self.normalized_name) if container_is_running: stopped = stop_container(self.normalized_name) if not stopped: raise Warning('Atención', 'No se pudo parar el contenedor %s' % self.normalized_name) # 2. Delete dirs self._delete_dirs(self.normalized_name) # 3. Drop database self._drop_database(self.normalized_name) self.state = 'destroyed' self.running = False @api.one def action_start(self): if self.running: raise Warning('Atención', 'No se puede arrancar una instancia que ya está arrancada') # 1. Get container status container_is_running = get_status(self.normalized_name) if container_is_running: raise Warning('Atención', 'No se puede arrancar una instancia que ya está arrancada') # 2. Start container started = start_container(self.normalized_name) if not started: raise Warning('Atención', 'No se pudo arrancar el contenedor') self.running = True @api.one def action_restart(self): if not self.running: raise Warning('Atención', 'No se puede parar y arrancar una instancia que ya está parada') # 1. Get container status container_is_running = get_status(self.normalized_name) if not container_is_running: raise Warning('Atención', 'No se puede parar y arrancar una instancia que ya está parada') # 2. Restart container restarted = restart_container(self.normalized_name) if not restarted: raise Warning('Atención', 'No se pudo reiniciar el contenedor') self.running = True @api.model def check_status(self): print('croned')