import psycopg2 from odoo import models, fields, api, _ from odoo.exceptions import UserError import secrets import string import subprocess import logging from datetime import datetime, timedelta _logger = logging.getLogger(__name__) class DatabaseInstance(models.Model): _name = 'database.instance' _description = 'Database Instance' _rec_name = 'database_name' subscription_id = fields.Many2one('user.subscription', string='Subscription', required=True, ondelete='cascade') database_name = fields.Char(string='Database Name', required=True, index=True) subdomain = fields.Char(string='Subdomain', required=True) full_domain = fields.Char(string='Full Domain', compute='_compute_full_domain') # Database status db_state = fields.Selection([ ('pending', 'Pending'), ('creating', 'Creating'), ('installing', 'Installing Apps'), ('ready', 'Ready'), ('failed', 'Failed'), ], string='Status', default='pending', tracking=True) # PostgreSQL connection pg_host = fields.Char(string='PostgreSQL Host', default='localhost') pg_port = fields.Integer(string='PostgreSQL Port', default=5432) pg_user = fields.Char(string='Database User') pg_password = fields.Char(string='Database Password') # Instance configuration workers = fields.Integer(string='Workers', default=2) timeout = fields.Integer(string='Timeout (seconds)', default=300) memory_limit_mb = fields.Integer(string='Memory Limit (MB)', default=512) max_connections = fields.Integer(string='Max Connections', default=20) # Timestamps created_at = fields.Datetime(string='Created At', default=fields.Datetime.now) ready_at = fields.Datetime(string='Ready At') last_accessed = fields.Datetime(string='Last Accessed') # Admin credentials admin_login = fields.Char(string='Admin Login') admin_password = fields.Char(string='Admin Password') admin_email = fields.Char(string='Admin Email') installed_apps = fields.Text(string='Installed Apps') error_message = fields.Text(string='Error Message') @api.depends('subdomain') def _compute_full_domain(self): base_domain = self.env['ir.config_parameter'].sudo().get_param('web.base.domain', 'localhost') for record in self: if record.subdomain: record.full_domain = f"{record.subdomain}.{base_domain}" else: record.full_domain = False def generate_unique_subdomain(self, company_name): """Generate unique subdomain from company name""" import re # Convert to lowercase and remove special characters base = re.sub(r'[^a-z0-9]+', '-', company_name.lower()).strip('-') # Add random string random_str = ''.join(secrets.choice(string.digits) for _ in range(5)) subdomain = f"{base}-{random_str}" # Ensure uniqueness counter = 1 original_subdomain = subdomain while self.search_count([('subdomain', '=', subdomain)]) > 0: subdomain = f"{original_subdomain}-{counter}" counter += 1 return subdomain def generate_password(self, length=12): """Generate secure random password""" chars = string.ascii_letters + string.digits + "!@#$%^&*" return ''.join(secrets.choice(chars) for _ in range(length)) def create_database(self): """Create PostgreSQL database for tenant""" self.ensure_one() try: self.db_state = 'creating' self._cr.commit() # Generate credentials self.pg_user = f"user_{self.database_name.replace('-', '_')}" self.pg_password = self.generate_password() self.admin_login = 'admin' self.admin_password = self.generate_password() # Connect to PostgreSQL conn = psycopg2.connect( host=self.pg_host, port=self.pg_port, database='postgres', user='odoo', # Master PostgreSQL user password='odoo_password' # Should be in config ) conn.autocommit = True cursor = conn.cursor() # Create database user try: cursor.execute(f"DROP ROLE IF EXISTS {self.pg_user}") cursor.execute( f"CREATE ROLE {self.pg_user} WITH LOGIN PASSWORD %s", (self.pg_password,) ) except Exception as e: _logger.error(f"Error creating user: {e}") # Create database cursor.execute(f"DROP DATABASE IF EXISTS {self.database_name}") cursor.execute( f"CREATE DATABASE {self.database_name} " f"OWNER {self.pg_user} " f"ENCODING 'UTF8' " f"TEMPLATE template0" ) # Set permissions cursor.execute( f"GRANT ALL PRIVILEGES ON DATABASE {self.database_name} TO {self.pg_user}" ) cursor.close() conn.close() _logger.info(f"Database {self.database_name} created successfully") return True except Exception as e: self.db_state = 'failed' self.error_message = str(e) _logger.error(f"Failed to create database: {e}") return False def install_apps(self, app_list): """Install selected Odoo apps in the database""" self.ensure_one() try: self.db_state = 'installing' self.installed_apps = ', '.join(app_list) self._cr.commit() # This would typically use Odoo's database provisioning API # For now, we'll simulate it import odoo from odoo import api, SUPERUSER_ID # Initialize new database registry = odoo.modules.registry.Registry.new( self.database_name, force_demo=False, signal=False, update_module=True ) with api.Environment.manage(), registry.cursor() as cr: env = api.Environment(cr, SUPERUSER_ID, {}) # Install selected modules for module_name in app_list: try: module = env['ir.module.module'].search([('name', '=', module_name)]) if module: module.button_immediate_install() _logger.info(f"Installed module: {module_name}") except Exception as e: _logger.warning(f"Failed to install {module_name}: {e}") # Create admin user env['res.users'].create({ 'name': 'Administrator', 'login': self.admin_login, 'password': self.admin_password, 'email': self.admin_email, 'groups_id': [(6, 0, [env.ref('base.group_system').id])], }) self.db_state = 'ready' self.ready_at = fields.Datetime.now() _logger.info(f"Database {self.database_name} is ready") return True except Exception as e: self.db_state = 'failed' self.error_message = str(e) _logger.error(f"Failed to install apps: {e}") return False def configure_instance(self): """Configure system instance parameters""" self.ensure_one() # Update odoo.conf or use environment variables config_params = { 'workers': self.workers, 'timeout': self.timeout, 'limit_memory_hard': self.memory_limit_mb * 1024 * 1024, 'max_cron_threads': 1, } # These would be applied at the Odoo server level _logger.info(f"Configuring instance {self.database_name}: {config_params}") return True